@anthusai/papyrus 1.4.0-next.1 → 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.
Files changed (121) hide show
  1. package/amplify/data/resource.js +134 -3
  2. package/amplify/data/resource.ts +145 -3
  3. package/amplify/data/schema.js +131 -0
  4. package/amplify/data/schema.ts +142 -0
  5. package/amplify/functions/category-action/handler.ts +272 -20
  6. package/amplify/functions/console-chat-responder/src/main.rs +1 -1
  7. package/amplify/functions/content-actions/handler.py +23 -0
  8. package/amplify/functions/content-actions/requirements.txt +1 -1
  9. package/amplify/functions/content-actions/resource.js +1 -1
  10. package/amplify/functions/content-actions/resource.ts +1 -1
  11. package/amplify/functions/renditions/requirements.txt +1 -1
  12. package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
  13. package/amplify/functions/shared/curation-delegation.ts +115 -0
  14. package/amplify/functions/shared/python-bundle.js +3 -3
  15. package/amplify/functions/shared/python-bundle.ts +3 -3
  16. package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
  17. package/amplify/functions/shared/slack-events.ts +2 -2
  18. package/amplify/publication/renditions-function.js +1 -1
  19. package/amplify/publication/renditions-function.ts +1 -1
  20. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
  21. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
  22. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
  23. package/app/[year]/[month]/[day]/page.tsx +3 -0
  24. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
  25. package/app/api/archive/editions/route.ts +2 -0
  26. package/app/api/auth/prospectus/accounts/route.ts +9 -0
  27. package/app/api/local-preview/derive/route.ts +28 -0
  28. package/app/api/local-preview/media/route.ts +25 -0
  29. package/app/archive/page.tsx +3 -0
  30. package/app/globals.css +28 -28
  31. package/app/layout.tsx +8 -5
  32. package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
  33. package/app/newsroom/[section]/page.tsx +3 -2
  34. package/app/newsroom/articles/page.tsx +8 -1
  35. package/app/newsroom/insights/[threadId]/page.tsx +5 -16
  36. package/app/newsroom/insights/page.tsx +4 -12
  37. package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
  38. package/app/page.tsx +30 -1
  39. package/components/administration-delegations-panel.tsx +156 -0
  40. package/components/curation-delegation-client.ts +104 -0
  41. package/components/item-stream-view.tsx +63 -0
  42. package/components/markus-ir-preview.tsx +47 -2
  43. package/components/news-desk-page.tsx +0 -1
  44. package/components/news-desk-taxonomy-client.ts +82 -242
  45. package/components/newsroom-article-editor.tsx +138 -40
  46. package/components/newsroom-assignment-detail.tsx +79 -0
  47. package/components/newsroom-assignments-view.tsx +1 -1
  48. package/components/newsroom-board-view.tsx +128 -51
  49. package/components/newsroom-newsprint-board.css +5 -46
  50. package/components/newsroom-ops-overview.tsx +27 -5
  51. package/components/newsroom-organizer-shell.tsx +26 -8
  52. package/components/newsroom-organizer.css +267 -12
  53. package/components/newsroom-references-view.tsx +100 -23
  54. package/components/newsroom-topics-view.tsx +1 -1
  55. package/components/priming-proof-page.tsx +17 -8
  56. package/components/prospectus-account-chooser.tsx +131 -0
  57. package/components/reference-cyclotron-status.tsx +5 -5
  58. package/components/reference-cyclotron-suggestion.tsx +143 -0
  59. package/components/reference-live-agreement.tsx +62 -0
  60. package/components/reference-section-suggestion.tsx +104 -0
  61. package/components/story-reporting-trail-client.ts +91 -0
  62. package/components/story-reporting-trail.tsx +115 -0
  63. package/components/topic-steering-workspace.tsx +245 -678
  64. package/components/ui/sheet.tsx +10 -4
  65. package/components/use-paged-content-item-versions.ts +151 -0
  66. package/components/wiki-concepts-view.tsx +153 -0
  67. package/components/wiki-index-client.ts +51 -0
  68. package/infra/amplify-app-shell.js +17 -3
  69. package/infra/amplify-app-shell.ts +18 -3
  70. package/infra/site-config.js +6 -1
  71. package/infra/site-config.ts +7 -1
  72. package/lib/approved-priming-proof-reports.ts +6 -3
  73. package/lib/approved-priming-proofs/papyrus.json +90 -0
  74. package/lib/blog-feature-solver.ts +1 -109
  75. package/lib/category-repository.ts +1 -1
  76. package/lib/content-actions-client.ts +3 -0
  77. package/lib/content-actions-demo.ts +55 -0
  78. package/lib/content-item-lifecycle.ts +34 -5
  79. package/lib/content-source-context.ts +9 -1
  80. package/lib/curation-delegation-helpers.ts +69 -0
  81. package/lib/cyclotron-suggestions.ts +263 -0
  82. package/lib/front-matter-fields.ts +42 -0
  83. package/lib/front-mosaic.ts +2 -1
  84. package/lib/graphql-content-repository.ts +42 -7
  85. package/lib/item-stream.ts +47 -0
  86. package/lib/layout-plan.ts +3 -2
  87. package/lib/layout-warnings.ts +8 -0
  88. package/lib/local-preview-section-fixtures.ts +72 -0
  89. package/lib/local-preview-seed.ts +196 -7
  90. package/lib/local-preview-tenant.ts +27 -0
  91. package/lib/newsroom-articles.ts +2 -0
  92. package/lib/newsroom-assignments.ts +14 -1
  93. package/lib/newsroom-board.ts +52 -7
  94. package/lib/newsroom-demo-dashboard.ts +1 -1
  95. package/lib/newsroom-forum-routes.ts +2 -43
  96. package/lib/newsroom-index-filters.ts +3 -40
  97. package/lib/newsroom-media-backend.ts +5 -0
  98. package/lib/newsroom-nav.ts +1 -3
  99. package/lib/newsroom-organizer-nav.ts +8 -14
  100. package/lib/newsroom-section-routing.ts +1 -1
  101. package/lib/newsroom-sections.ts +12 -0
  102. package/lib/papyrus-web-locations.ts +3 -24
  103. package/lib/priming-proof-report.ts +1 -0
  104. package/lib/prospectus-account-client.ts +90 -0
  105. package/lib/prospectus-sign-in-routes.ts +45 -0
  106. package/lib/prospectus-site-identity.ts +13 -1
  107. package/lib/prospectus-tenant.ts +22 -0
  108. package/lib/reader-static-params.ts +2 -1
  109. package/lib/relevance-decisions.ts +29 -0
  110. package/lib/reporting-story-budget.ts +2 -2
  111. package/lib/semantic-graph.ts +5 -5
  112. package/lib/site-brand.ts +20 -0
  113. package/lib/site-env.ts +5 -0
  114. package/lib/site-section-suggestions.ts +120 -0
  115. package/lib/site-stack.ts +1 -1
  116. package/lib/story-reporting-trail.ts +106 -0
  117. package/lib/tenant-keys.ts +32 -0
  118. package/lib/wiki.ts +56 -0
  119. package/package.json +34 -2
  120. package/renderers/pretext/rhythm-article-page.tsx +1 -18
  121. package/routes.manifest.json +34 -7
@@ -0,0 +1,115 @@
1
+ import { createHash, randomBytes, randomInt } from "node:crypto"
2
+
3
+ export const CURATION_DELEGATION_PAYLOAD_KIND = "curation-delegation"
4
+ export const CURATION_DELEGATION_OWNER_TYPE = "curationDelegation"
5
+ export const DEFAULT_DELEGATION_HOURS = 8
6
+ export const MAXIMUM_DELEGATION_HOURS = 72
7
+
8
+ export type CurationDelegation = {
9
+ delegationId: string
10
+ tokenHash: string
11
+ grantorSub: string
12
+ grantorLabel: string
13
+ agentLabel: string
14
+ createdAt: string
15
+ expiresAt: string
16
+ revokedAt: string | null
17
+ userCode?: string | null
18
+ requestedHours?: number | null
19
+ pendingExpiresAt?: string | null
20
+ approvedAt?: string | null
21
+ deniedAt?: string | null
22
+ }
23
+
24
+ export type DelegationState = "pending" | "approved" | "denied" | "expired" | "revoked"
25
+
26
+ export type DelegationVerdict =
27
+ | { ok: true }
28
+ | { ok: false; reason: "unknown-token" | "wrong-agent" | "revoked" | "not-approved" | "expired" }
29
+
30
+ export function hashDelegationToken(token: string): string {
31
+ return createHash("sha256").update(token, "utf8").digest("hex")
32
+ }
33
+
34
+ export function newDelegationToken(delegationId: string): { token: string; tokenHash: string } {
35
+ const token = `${delegationId}~${randomBytes(24).toString("base64url")}`
36
+ return { token, tokenHash: hashDelegationToken(token) }
37
+ }
38
+
39
+ export function delegationIdFromToken(token: string): string | null {
40
+ const separator = token.indexOf("~")
41
+ return separator > 0 ? token.slice(0, separator) : null
42
+ }
43
+
44
+ export function delegationExpiry(now: string, hours: number | null | undefined): string {
45
+ const requested = hours ?? DEFAULT_DELEGATION_HOURS
46
+ if (!Number.isFinite(requested) || requested <= 0 || requested > MAXIMUM_DELEGATION_HOURS) {
47
+ throw new Error(`A delegation lasts between 1 and ${MAXIMUM_DELEGATION_HOURS} hours.`)
48
+ }
49
+ return new Date(Date.parse(now) + requested * 3_600_000).toISOString()
50
+ }
51
+
52
+ export function delegationVerdict(
53
+ delegation: CurationDelegation | null,
54
+ presented: { token: string; agentLabel: string; now: string },
55
+ ): DelegationVerdict {
56
+ if (!delegation || delegation.tokenHash !== hashDelegationToken(presented.token)) {
57
+ return { ok: false, reason: "unknown-token" }
58
+ }
59
+ if (delegation.agentLabel.trim().toLowerCase() !== presented.agentLabel.trim().toLowerCase()) {
60
+ return { ok: false, reason: "wrong-agent" }
61
+ }
62
+ if (delegation.revokedAt) return { ok: false, reason: "revoked" }
63
+ const state = delegationState(delegation, presented.now)
64
+ if (state === "pending" || state === "denied" || (state === "expired" && delegation.userCode && !delegation.approvedAt)) {
65
+ return { ok: false, reason: "not-approved" }
66
+ }
67
+ if (Date.parse(delegation.expiresAt) <= Date.parse(presented.now)) return { ok: false, reason: "expired" }
68
+ return { ok: true }
69
+ }
70
+
71
+ export function delegationRefusalMessage(reason: Exclude<DelegationVerdict, { ok: true }>["reason"]): string {
72
+ return {
73
+ "unknown-token": "That delegation token is not recognized.",
74
+ "wrong-agent": "That delegation was granted to a different agent.",
75
+ "revoked": "That delegation was revoked.",
76
+ "not-approved": "That delegation has not been approved by a person. Ask them to approve the code in the Newsroom.",
77
+ "expired": "That delegation has expired. Ask the person for a new one.",
78
+ }[reason]
79
+ }
80
+
81
+ export const PENDING_DELEGATION_MINUTES = 10
82
+ export const MAXIMUM_PENDING_DELEGATION_REQUESTS = 5
83
+ export const PENDING_REQUEST_GRANTOR_SUB = "pending-device-authorization"
84
+ export const EXPIRED_REQUEST_GRANTOR_SUB = "expired-device-authorization"
85
+ export const USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
86
+ export const USER_CODE_LENGTH = 8
87
+
88
+ export function newUserCode(): string {
89
+ let code = ""
90
+ for (let position = 0; position < USER_CODE_LENGTH; position += 1) {
91
+ code += USER_CODE_ALPHABET[randomInt(USER_CODE_ALPHABET.length)]
92
+ }
93
+ return code
94
+ }
95
+
96
+ export function formatUserCode(code: string): string {
97
+ return `${code.slice(0, 4)}-${code.slice(4)}`
98
+ }
99
+
100
+ export function normalizeUserCode(typed: string | null | undefined): string | null {
101
+ const stripped = (typed ?? "").toUpperCase().replace(/[\s-]/g, "")
102
+ if (stripped.length !== USER_CODE_LENGTH) return null
103
+ for (const character of stripped) if (!USER_CODE_ALPHABET.includes(character)) return null
104
+ return stripped
105
+ }
106
+
107
+ export function delegationState(delegation: CurationDelegation, now: string): DelegationState {
108
+ if (delegation.revokedAt) return "revoked"
109
+ if (delegation.deniedAt) return "denied"
110
+ if (delegation.userCode && !delegation.approvedAt) {
111
+ const pendingEnd = delegation.pendingExpiresAt ?? delegation.expiresAt
112
+ return Date.parse(pendingEnd) <= Date.parse(now) ? "expired" : "pending"
113
+ }
114
+ return Date.parse(delegation.expiresAt) <= Date.parse(now) ? "expired" : "approved"
115
+ }
@@ -126,9 +126,9 @@ function bundleFromPip(outputDir, options) {
126
126
  }
127
127
  export function bundleFromCheckout(outputDir, options) {
128
128
  fs.copyFileSync(path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"), path.join(outputDir, "handler.py"));
129
- for (const module of options.modules) {
130
- fs.mkdirSync(path.join(outputDir, module), { recursive: true });
131
- fs.cpSync(path.join(packageRoot, "src", module), path.join(outputDir, module), { recursive: true });
129
+ for (const moduleName of options.modules) {
130
+ fs.mkdirSync(path.join(outputDir, moduleName), { recursive: true });
131
+ fs.cpSync(path.join(packageRoot, "src", moduleName), path.join(outputDir, moduleName), { recursive: true });
132
132
  }
133
133
  options.checkoutExtras?.(outputDir);
134
134
  copySiteFiles(outputDir, options.siteFiles);
@@ -164,9 +164,9 @@ export function bundleFromCheckout(outputDir: string, options: PythonBundleOptio
164
164
  path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"),
165
165
  path.join(outputDir, "handler.py"),
166
166
  );
167
- for (const module of options.modules) {
168
- fs.mkdirSync(path.join(outputDir, module), { recursive: true });
169
- fs.cpSync(path.join(packageRoot, "src", module), path.join(outputDir, module), { recursive: true });
167
+ for (const moduleName of options.modules) {
168
+ fs.mkdirSync(path.join(outputDir, moduleName), { recursive: true });
169
+ fs.cpSync(path.join(packageRoot, "src", moduleName), path.join(outputDir, moduleName), { recursive: true });
170
170
  }
171
171
  options.checkoutExtras?.(outputDir);
172
172
  copySiteFiles(outputDir, options.siteFiles);
@@ -12,6 +12,22 @@ export type ReferenceCurationMetadataInput = {
12
12
  referenceLineageId: string
13
13
  decisionRelationId?: string | null
14
14
  shareable?: boolean | null
15
+ actorKind?: CurationActorKind | null
16
+ sectionId?: string | null
17
+ delegation?: { delegationId: string; relayedBy: string; instruction: string } | null
18
+ }
19
+
20
+ export type CurationActorKind = "human" | "service"
21
+
22
+ /**
23
+ * A curation is human only when the caller signed in through the user pool, so
24
+ * AppSync supplied a verified subject claim. Request arguments never decide it;
25
+ * IAM-signed service callers and agents have no such claim.
26
+ */
27
+ export function curationActorKind(identity: unknown): CurationActorKind {
28
+ const claims = (identity as {claims?: Record<string, unknown>} | null | undefined)?.claims
29
+ const subject = claims?.sub
30
+ return typeof subject === "string" && subject.trim() !== "" ? "human" : "service"
15
31
  }
16
32
 
17
33
  export function referenceCurationMetadata(input: ReferenceCurationMetadataInput): Record<string, unknown> {
@@ -23,7 +39,42 @@ export function referenceCurationMetadata(input: ReferenceCurationMetadataInput)
23
39
  curationReasonCode: input.reasonCode,
24
40
  referenceId: input.referenceId,
25
41
  referenceLineageId: input.referenceLineageId,
42
+ ...(input.actorKind ? {actorKind: input.actorKind} : {}),
43
+ ...(input.sectionId?.trim() ? {sectionId: input.sectionId.trim()} : {}),
44
+ ...(input.delegation
45
+ ? {attestation: "delegated", relayedBy: input.delegation.relayedBy, delegationId: input.delegation.delegationId,
46
+ instruction: input.delegation.instruction}
47
+ : {}),
26
48
  ...(decisionRelationId ? {decisionRelationId} : {}),
27
49
  ...(decisionRelationId ? {shareable: input.shareable === true} : {}),
28
50
  }
29
51
  }
52
+
53
+ export type CurationSectionCandidate = {
54
+ id?: string | null
55
+ enabled?: boolean | null
56
+ enabledStatus?: string | null
57
+ } | null | undefined
58
+
59
+ /**
60
+ * A curation may name the newsroom Section the Reference belongs in, but only
61
+ * when it accepts the Reference and the Section exists and is enabled.
62
+ */
63
+ export function validatedCurationSectionId(
64
+ requestedSectionId: string | null | undefined,
65
+ action: string,
66
+ section: CurationSectionCandidate,
67
+ ): string | null {
68
+ const sectionId = requestedSectionId?.trim() || null
69
+ if (!sectionId) return null
70
+ if (action !== "accept") {
71
+ throw new Error("A section can be recorded only when accepting a reference.")
72
+ }
73
+ if (!section || section.id !== sectionId) {
74
+ throw new Error(`Newsroom section ${sectionId} was not found.`)
75
+ }
76
+ if (section.enabled === false || section.enabledStatus === "disabled") {
77
+ throw new Error(`Newsroom section ${sectionId} is disabled.`)
78
+ }
79
+ return sectionId
80
+ }
@@ -69,9 +69,9 @@ export function slackAgentInstructions(): string {
69
69
  "",
70
70
  "Be concise, accurate, and concrete. Slack has no web UI: do not use papyrus.web.navigate, papyrus.web.current_location, or assume the user can see papyrus:// pages.",
71
71
  "",
72
- "Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable insight, recommend creating an insight Message instead of making every chat turn canonical knowledge.",
72
+ "Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable note, recommend creating a note Message instead of making every chat turn canonical knowledge.",
73
73
  "",
74
- "Respond openly to questions, commands, and discussion—the same conversational stance as the web console. Do not treat every message as a citation submission. Register references or create insights only when the user shares URLs/DOIs or asks you to file, summarize, or comment on specific material.",
74
+ "Respond openly to questions, commands, and discussion—the same conversational stance as the web console. Do not treat every message as a citation submission. Register references or create notes only when the user shares URLs/DOIs or asks you to file, summarize, or comment on specific material.",
75
75
  "",
76
76
  'For requests like "most recent references" or "tell me about recent references", do not ask clarifying questions first: immediately call execute_tactus with a Reference.list snippet, then summarize the results.',
77
77
  "",
@@ -11,7 +11,7 @@ import { pythonLambdaCode } from "../functions/shared/python-bundle.js";
11
11
  import { RENDITIONS_EPHEMERAL_STORAGE_MB, RENDITIONS_MEDIA_PREFIX, RENDITIONS_MEMORY_MB, RENDITIONS_TIMEOUT_SECONDS, renditionsEnvironment, renditionsEventPattern, } from "./renditions-settings.js";
12
12
  const dirname = path.dirname(fileURLToPath(import.meta.url));
13
13
  const projectRoot = path.resolve(dirname, "../..");
14
- export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.5.1", "Pillow>=11.3,<12"];
14
+ export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12"];
15
15
  function installCheckoutRequirements(outputDir) {
16
16
  const requirementsPath = path.join(projectRoot, "amplify/functions/renditions/requirements.txt");
17
17
  execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
@@ -22,7 +22,7 @@ import {
22
22
  const dirname = path.dirname(fileURLToPath(import.meta.url));
23
23
  const projectRoot = path.resolve(dirname, "../..");
24
24
 
25
- export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.5.1", "Pillow>=11.3,<12"];
25
+ export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12"];
26
26
 
27
27
  export type RenditionsFunctionProps = {
28
28
  siteBucket: s3.IBucket;
@@ -3,7 +3,7 @@ import { getSiteRenderer } from "../../../../../lib/site-renderer";
3
3
  import { getCachedEditionItem, loadCachedEditionContent } from "../../../../../lib/cached-content-repository";
4
4
  import { getEditionDatePath, parseEditionArticleRoute } from "../../../../../lib/edition-routes";
5
5
  import { buildPresentationFooterEntries } from "../../../../../lib/presentation-footer";
6
- import { SITE_BRAND } from "../../../../../lib/site-brand";
6
+ import { SITE_BRAND, editionsEnabled } from "../../../../../lib/site-brand";
7
7
  import { generateEditionItemStaticParams } from "../../../../../lib/reader-static-params";
8
8
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
9
9
  export const revalidate = 60;
@@ -39,6 +39,7 @@ export async function generateMetadata({ params }: DateScopedArticlePageProps) {
39
39
  }
40
40
 
41
41
  export default async function DateScopedArticlePage({ params }: DateScopedArticlePageProps) {
42
+ if (!editionsEnabled()) notFound();
42
43
  const { year, month, day, articleSlug } = await params;
43
44
  const route = parseEditionArticleRoute({ year, month, day, articleSlug });
44
45
  if (!route) notFound();
@@ -1,4 +1,5 @@
1
1
  import { notFound, redirect } from "next/navigation";
2
+ import { editionsEnabled } from "../../../../lib/site-brand";
2
3
  import { siteServesNextReaderRoutes } from "../../../../lib/reader-static-params";
3
4
  import { getSiteRenderer } from "../../../../lib/site-renderer";
4
5
  import type { PresentationTarget } from "../../../../renderers/pretext";
@@ -23,7 +24,7 @@ export async function EditionRoutePage({
23
24
  lockedPresentation,
24
25
  target = { kind: "edition" },
25
26
  }: EditionRoutePageProps) {
26
- if (!siteServesNextReaderRoutes()) notFound();
27
+ if (!editionsEnabled() || !siteServesNextReaderRoutes()) notFound();
27
28
  const route = parseEditionDateRoute({ year, month, day });
28
29
  if (!route) notFound();
29
30
  if (!route.isCanonical) redirect(route.canonicalPath);
@@ -1,7 +1,7 @@
1
1
  import { notFound, redirect } from "next/navigation";
2
2
  import { EditionRoutePage } from "../../edition-route-page";
3
3
  import { parseEditionPageRoute } from "../../../../../../lib/edition-routes";
4
- import { getDefaultPretextLayout, getForcedPresentation } from "../../../../../../lib/site-brand";
4
+ import { editionsEnabled, getDefaultPretextLayout, getForcedPresentation } from "../../../../../../lib/site-brand";
5
5
  import { generateEditionPageStaticParams } from "../../../../../../lib/reader-static-params";
6
6
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
7
7
  export const revalidate = 60;
@@ -20,6 +20,7 @@ type EditionPageRouteProps = {
20
20
  };
21
21
 
22
22
  export default async function EditionPageRoute({ params }: EditionPageRouteProps) {
23
+ if (!editionsEnabled()) notFound();
23
24
  const { year, month, day, pageNumber } = await params;
24
25
  const route = parseEditionPageRoute({ year, month, day, pageNumber });
25
26
  if (!route) notFound();
@@ -1,3 +1,5 @@
1
+ import { notFound } from "next/navigation";
2
+ import { editionsEnabled } from "../../../../lib/site-brand";
1
3
  import { EditionRoutePage } from "./edition-route-page";
2
4
  import { generateEditionDateStaticParams } from "../../../../lib/reader-static-params";
3
5
 
@@ -17,6 +19,7 @@ type DateEditionPageProps = {
17
19
  };
18
20
 
19
21
  export default async function DateEditionPage({ params }: DateEditionPageProps) {
22
+ if (!editionsEnabled()) notFound();
20
23
  const { year, month, day } = await params;
21
24
  return <EditionRoutePage day={day} month={month} year={year} />;
22
25
  }
@@ -2,6 +2,7 @@ import { notFound, redirect } from "next/navigation";
2
2
  import { EditionRoutePage } from "../../edition-route-page";
3
3
  import { createSectionKey } from "../../../../../../lib/edition-sections";
4
4
  import { parseEditionSectionRoute } from "../../../../../../lib/edition-routes";
5
+ import { editionsEnabled } from "../../../../../../lib/site-brand";
5
6
  import { generateEditionSectionStaticParams } from "../../../../../../lib/reader-static-params";
6
7
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
7
8
  export const revalidate = 60;
@@ -20,6 +21,7 @@ type EditionSectionRouteProps = {
20
21
  };
21
22
 
22
23
  export default async function EditionSectionRoute({ params }: EditionSectionRouteProps) {
24
+ if (!editionsEnabled()) notFound();
23
25
  const { year, month, day, sectionKey } = await params;
24
26
  const normalizedSectionKey = createSectionKey(sectionKey);
25
27
  const route = parseEditionSectionRoute({ year, month, day, sectionKey: normalizedSectionKey });
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { editionsEnabled } from "../../../../lib/site-brand";
2
3
  import { loadArchiveEditionPreviews, clampArchiveLimit } from "../../../../lib/archive-data";
3
4
  import { ARCHIVE_BATCH_SIZE } from "../../../../lib/archive-types";
4
5
 
@@ -6,6 +7,7 @@ import { ARCHIVE_BATCH_SIZE } from "../../../../lib/archive-types";
6
7
  export const revalidate = 60;
7
8
 
8
9
  export async function GET(request: Request) {
10
+ if (!editionsEnabled()) return new NextResponse(null, { status: 404 });
9
11
  const url = new URL(request.url);
10
12
  const cursor = url.searchParams.get("cursor");
11
13
  const requestedLimit = Number(url.searchParams.get("limit") ?? ARCHIVE_BATCH_SIZE);
@@ -0,0 +1,9 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleProspectusPublishingAccountCreation } from "../../../../../lib/prospectus-sign-in-routes";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function POST(request: NextRequest) {
8
+ return handleProspectusPublishingAccountCreation(request);
9
+ }
@@ -0,0 +1,28 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { NextResponse } from "next/server";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ /**
8
+ * Local preview only: derives a story's Markus body through the real Python engine so directives render.
9
+ * Set PAPYRUS_PREVIEW_PYTHON to a Python that has Papyrus and its Markus extra installed.
10
+ */
11
+ export async function POST(request: Request) {
12
+ if (process.env.NEXT_PUBLIC_PAPYRUS_LOCAL_PREVIEW !== "1") return new NextResponse(null, { status: 404 });
13
+ const python = process.env.PAPYRUS_PREVIEW_PYTHON?.trim();
14
+ if (!python) return NextResponse.json({ error: "PAPYRUS_PREVIEW_PYTHON is not set." }, { status: 501 });
15
+ const input = await request.text();
16
+ const script = path.join(process.cwd(), "scripts", "local-preview-derive.py");
17
+ const output = await new Promise<string>((resolve, reject) => {
18
+ const child = spawn(python, [script], { env: process.env });
19
+ let stdout = "";
20
+ let stderr = "";
21
+ child.stdout.on("data", chunk => { stdout += chunk; });
22
+ child.stderr.on("data", chunk => { stderr += chunk; });
23
+ child.on("error", reject);
24
+ child.on("close", code => code === 0 ? resolve(stdout) : reject(new Error(stderr || `derive exited with ${code}`)));
25
+ child.stdin.end(input);
26
+ }).catch((reason: unknown) => JSON.stringify({ error: reason instanceof Error ? reason.message : "derive failed" }));
27
+ return new NextResponse(output, { headers: { "content-type": "application/json" } });
28
+ }
@@ -0,0 +1,25 @@
1
+ import { spawn } from "node:child_process";
2
+ import { NextResponse } from "next/server";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ const CONTENT_TYPES: Record<string, string> = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", avif: "image/avif" };
7
+
8
+ /**
9
+ * Local preview only: streams one story image from the publication's storage bucket so the previews show real figures.
10
+ * Set PAPYRUS_PREVIEW_MEDIA_BUCKET to the bucket; the server's AWS credentials do the reading.
11
+ */
12
+ export async function GET(request: Request) {
13
+ if (process.env.NEXT_PUBLIC_PAPYRUS_LOCAL_PREVIEW !== "1") return new NextResponse(null, { status: 404 });
14
+ const bucket = process.env.PAPYRUS_PREVIEW_MEDIA_BUCKET?.trim();
15
+ const key = new URL(request.url).searchParams.get("key") ?? "";
16
+ if (!bucket) return NextResponse.json({ error: "PAPYRUS_PREVIEW_MEDIA_BUCKET is not set." }, { status: 501 });
17
+ if (!/^media\/[A-Za-z0-9_\-./]+$/.test(key) || key.includes("..")) return new NextResponse(null, { status: 400 });
18
+ const extension = key.split(".").pop()?.toLowerCase() ?? "";
19
+ const child = spawn("aws", ["s3", "cp", `s3://${bucket}/${key}`, "-"], { env: process.env });
20
+ const chunks: Buffer[] = [];
21
+ child.stdout.on("data", chunk => chunks.push(chunk));
22
+ const exitCode = await new Promise<number>(resolve => { child.on("close", code => resolve(code ?? 1)); child.on("error", () => resolve(1)); });
23
+ if (exitCode !== 0) return new NextResponse(null, { status: 404 });
24
+ return new NextResponse(Buffer.concat(chunks), { headers: { "content-type": CONTENT_TYPES[extension] ?? "application/octet-stream", "cache-control": "private, max-age=300" } });
25
+ }
@@ -1,3 +1,5 @@
1
+ import { notFound } from "next/navigation";
2
+ import { editionsEnabled } from "../../lib/site-brand";
1
3
  import { ArchiveShell } from "../../components/archive-shell";
2
4
  import { ArchiveGrid } from "../../components/archive-grid";
3
5
  import { loadArchiveEditionPreviews } from "../../lib/archive-data";
@@ -7,6 +9,7 @@ import { ARCHIVE_BATCH_SIZE } from "../../lib/archive-types";
7
9
  export const revalidate = 60;
8
10
 
9
11
  export default async function ArchivePage() {
12
+ if (!editionsEnabled()) notFound();
10
13
  const initialBatch = await loadArchiveEditionPreviews({ limit: ARCHIVE_BATCH_SIZE });
11
14
 
12
15
  return (
package/app/globals.css CHANGED
@@ -6275,7 +6275,7 @@ img {
6275
6275
  padding-top: calc(var(--paper-rhythm) - 1px);
6276
6276
  }
6277
6277
 
6278
- .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-topic-workflow] + [data-reference-insight-workflow] {
6278
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-topic-workflow] + [data-reference-note-workflow] {
6279
6279
  border-top: 0;
6280
6280
  padding-top: 0;
6281
6281
  }
@@ -6476,7 +6476,7 @@ img {
6476
6476
 
6477
6477
  .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-workflow__input button,
6478
6478
  .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-workflow__header > button,
6479
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > button {
6479
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > button {
6480
6480
  background: transparent;
6481
6481
  border: 1px solid rgb(var(--rule-rgb) / 0.42);
6482
6482
  box-sizing: border-box;
@@ -6498,7 +6498,7 @@ img {
6498
6498
  }
6499
6499
 
6500
6500
  .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-workflow__header > button:disabled,
6501
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > button:disabled {
6501
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > button:disabled {
6502
6502
  cursor: not-allowed;
6503
6503
  opacity: 0.45;
6504
6504
  }
@@ -6521,26 +6521,26 @@ img {
6521
6521
  margin-right: calc(var(--paper-rhythm) * 0.5);
6522
6522
  }
6523
6523
 
6524
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form {
6524
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form {
6525
6525
  display: grid;
6526
6526
  gap: 0;
6527
6527
  width: 100%;
6528
6528
  }
6529
6529
 
6530
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell {
6530
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell {
6531
6531
  display: grid;
6532
6532
  gap: 0;
6533
6533
  }
6534
6534
 
6535
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > * + * {
6535
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > * + * {
6536
6536
  margin-top: var(--paper-rhythm);
6537
6537
  }
6538
6538
 
6539
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form > * + * {
6539
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form > * + * {
6540
6540
  margin-top: var(--paper-rhythm);
6541
6541
  }
6542
6542
 
6543
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > button {
6543
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > button {
6544
6544
  align-items: center;
6545
6545
  appearance: none;
6546
6546
  display: inline-flex;
@@ -6550,7 +6550,7 @@ img {
6550
6550
  vertical-align: top;
6551
6551
  }
6552
6552
 
6553
- .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-insight-workflow] .news-desk-reference-workflow__header > button {
6553
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-note-workflow] .news-desk-reference-workflow__header > button {
6554
6554
  align-items: center;
6555
6555
  appearance: none;
6556
6556
  display: inline-flex;
@@ -6562,37 +6562,37 @@ img {
6562
6562
  vertical-align: top;
6563
6563
  }
6564
6564
 
6565
- .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-insight-workflow] .news-desk-reference-workflow__header {
6565
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-note-workflow] .news-desk-reference-workflow__header {
6566
6566
  align-items: flex-start;
6567
6567
  }
6568
6568
 
6569
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list {
6569
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list {
6570
6570
  display: grid;
6571
6571
  gap: 0;
6572
6572
  }
6573
6573
 
6574
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row {
6574
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row {
6575
6575
  border-left: 0;
6576
6576
  gap: 0;
6577
6577
  padding-left: 0;
6578
6578
  }
6579
6579
 
6580
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row + .news-desk-reference-topic-state-row {
6580
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row + .news-desk-reference-topic-state-row {
6581
6581
  margin-top: var(--paper-rhythm);
6582
6582
  }
6583
6583
 
6584
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row__headline,
6585
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row__meta {
6584
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row__headline,
6585
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row__meta {
6586
6586
  gap: 0;
6587
6587
  }
6588
6588
 
6589
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row strong,
6590
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row span {
6589
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row strong,
6590
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row span {
6591
6591
  line-height: var(--paper-rhythm);
6592
6592
  min-height: var(--paper-rhythm);
6593
6593
  }
6594
6594
 
6595
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form .news-desk-reference-curation-note {
6595
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form .news-desk-reference-curation-note {
6596
6596
  display: grid;
6597
6597
  gap: 0;
6598
6598
  grid-template-rows: var(--paper-rhythm) auto;
@@ -6600,7 +6600,7 @@ img {
6600
6600
  min-width: 0;
6601
6601
  }
6602
6602
 
6603
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form .news-desk-reference-curation-note span {
6603
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form .news-desk-reference-curation-note span {
6604
6604
  color: var(--muted-strong);
6605
6605
  font-family: Arial, Helvetica, sans-serif;
6606
6606
  font-size: 0.66rem;
@@ -6611,13 +6611,13 @@ img {
6611
6611
  text-transform: uppercase;
6612
6612
  }
6613
6613
 
6614
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell button {
6614
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell button {
6615
6615
  line-height: var(--paper-rhythm);
6616
6616
  min-height: var(--news-desk-detail-control-height);
6617
6617
  }
6618
6618
 
6619
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form .news-desk-reference-curation-note input,
6620
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form .news-desk-reference-curation-note textarea {
6619
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form .news-desk-reference-curation-note input,
6620
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form .news-desk-reference-curation-note textarea {
6621
6621
  box-sizing: border-box;
6622
6622
  display: block;
6623
6623
  font-family: Arial, Helvetica, sans-serif;
@@ -6627,7 +6627,7 @@ img {
6627
6627
  padding: 0 calc(var(--paper-rhythm) * 0.5);
6628
6628
  }
6629
6629
 
6630
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form textarea {
6630
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form textarea {
6631
6631
  background: rgb(var(--paper-rgb) / 0.42);
6632
6632
  border: 1px solid rgb(var(--rule-rgb) / 0.32);
6633
6633
  color: var(--ink);
@@ -6637,7 +6637,7 @@ img {
6637
6637
  resize: vertical;
6638
6638
  }
6639
6639
 
6640
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form__actions {
6640
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form__actions {
6641
6641
  align-items: center;
6642
6642
  column-gap: calc(var(--paper-rhythm) * 0.5);
6643
6643
  display: flex;
@@ -6646,7 +6646,7 @@ img {
6646
6646
  min-height: var(--paper-rhythm);
6647
6647
  }
6648
6648
 
6649
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form__actions button {
6649
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form__actions button {
6650
6650
  background: transparent;
6651
6651
  border: 1px solid rgb(var(--rule-rgb) / 0.42);
6652
6652
  box-sizing: border-box;
@@ -6662,7 +6662,7 @@ img {
6662
6662
  text-transform: uppercase;
6663
6663
  }
6664
6664
 
6665
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form__actions button:disabled {
6665
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form__actions button:disabled {
6666
6666
  cursor: not-allowed;
6667
6667
  opacity: 0.45;
6668
6668
  }
@@ -6895,11 +6895,11 @@ img {
6895
6895
  justify-self: stretch;
6896
6896
  }
6897
6897
 
6898
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > button {
6898
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > button {
6899
6899
  justify-self: stretch;
6900
6900
  }
6901
6901
 
6902
- .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-insight-workflow] .news-desk-reference-workflow__header {
6902
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-note-workflow] .news-desk-reference-workflow__header {
6903
6903
  align-items: flex-start;
6904
6904
  flex-direction: row;
6905
6905
  gap: var(--paper-rhythm);