@anthusai/papyrus 2.0.0-next.7 → 2.0.0-next.70
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 +23 -3
- package/amplify/data/resource.ts +25 -3
- package/amplify/data/schema.js +2028 -1979
- package/amplify/data/schema.ts +72 -14
- package/amplify/functions/assignment-action/data-access.js +90 -0
- package/amplify/functions/assignment-action/data-access.ts +89 -0
- package/amplify/functions/assignment-action/handler.ts +1 -1
- package/amplify/functions/content-actions/handler.py +61 -2
- package/amplify/functions/content-actions/requirements.txt +1 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +3 -3
- package/amplify/functions/editorial-checks/requirements.txt +1 -0
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
- package/amplify/functions/editorial-checks/schema.js +121 -0
- package/amplify/{plugins → functions}/editorial-checks/schema.ts +33 -6
- package/amplify/functions/editorial-checks/wire.js +53 -0
- package/amplify/functions/editorial-checks/wire.ts +61 -0
- package/amplify/functions/monitoring-view/handler.py +11 -0
- package/amplify/functions/monitoring-view/resource.js +24 -0
- package/amplify/functions/monitoring-view/resource.ts +34 -0
- package/amplify/functions/monitoring-view/schema.js +16 -0
- package/amplify/functions/monitoring-view/schema.ts +18 -0
- package/amplify/functions/monitoring-view/wire.js +37 -0
- package/amplify/functions/monitoring-view/wire.ts +47 -0
- package/amplify/functions/public-forms/core.js +247 -0
- package/amplify/functions/public-forms/core.ts +310 -0
- package/amplify/functions/public-forms/handler.ts +41 -0
- package/amplify/functions/public-forms/origins.js +35 -0
- package/amplify/functions/public-forms/origins.ts +43 -0
- package/amplify/functions/public-forms/resource.js +9 -0
- package/amplify/functions/public-forms/resource.ts +10 -0
- package/amplify/functions/public-forms/wire.js +25 -0
- package/amplify/functions/public-forms/wire.ts +32 -0
- package/amplify/functions/shared/editorial-context.ts +63 -4
- package/amplify/functions/shared/python-bundle.js +20 -14
- package/amplify/functions/shared/python-bundle.ts +25 -18
- package/amplify/plugins/newsletter/config.js +245 -0
- package/amplify/plugins/newsletter/config.ts +305 -0
- package/amplify/plugins/newsletter/function/handler.py +6 -0
- package/amplify/plugins/newsletter/function/requirements.txt +5 -0
- package/amplify/plugins/newsletter/ops/handler.py +12 -0
- package/amplify/plugins/newsletter/ops/resource.js +27 -0
- package/amplify/plugins/newsletter/ops/resource.ts +37 -0
- package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
- package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
- package/amplify/plugins/newsletter/schema.js +56 -0
- package/amplify/plugins/newsletter/schema.ts +61 -0
- package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
- package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
- package/amplify/plugins/newsletter/sending.js +140 -0
- package/amplify/plugins/newsletter/sending.ts +186 -0
- package/amplify/plugins/reader-steering/config.js +180 -0
- package/amplify/plugins/reader-steering/config.ts +280 -0
- package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
- package/amplify/plugins/reader-steering/function/handler.py +1554 -0
- package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
- package/amplify/plugins/reader-steering/function/moderation.py +316 -0
- package/amplify/plugins/reader-steering/function/proposals.py +255 -0
- package/amplify/plugins/reader-steering/function/resource.js +27 -0
- package/amplify/plugins/reader-steering/function/resource.ts +35 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +184 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +210 -0
- package/amplify/plugins/reader-steering/schema.js +204 -0
- package/amplify/plugins/reader-steering/schema.ts +206 -0
- package/amplify/plugins/registry.js +4 -2
- package/amplify/plugins/registry.ts +4 -2
- package/amplify/plugins/types.ts +5 -0
- package/amplify/plugins/wire.js +1 -0
- package/amplify/plugins/wire.ts +1 -0
- package/amplify/publication/projection-lease.js +30 -0
- package/amplify/publication/projection-lease.ts +39 -0
- package/amplify/publication/redirect-and-index.js +105 -0
- package/amplify/publication/redirect-rules.js +73 -0
- package/amplify/publication/redirect-rules.ts +93 -0
- package/amplify/publication/s3-projection.js +7 -1
- package/amplify/publication/s3-projection.ts +9 -1
- package/amplify/site-backend-config.ts +17 -0
- package/amplify/site-backend.js +38 -1
- package/amplify/site-backend.ts +46 -1
- package/app/api/auth/prospectus/sign-in/route.ts +4 -0
- package/app/api/newsletter/confirm/route.ts +13 -0
- package/app/api/newsletter/preferences/route.ts +13 -0
- package/app/api/newsletter/state/route.ts +9 -0
- package/app/api/newsletter/subscribe/route.ts +9 -0
- package/app/api/newsletter/unsubscribe/route.ts +13 -0
- package/app/api/newsroom/curation/context/route.ts +9 -0
- package/app/api/newsroom/curation/priming/route.ts +9 -0
- package/app/api/newsroom/curation/publication/route.ts +9 -0
- package/app/api/reader/comment/delete/route.ts +9 -0
- package/app/api/reader/comment/edit/route.ts +9 -0
- package/app/api/reader/comment/route.ts +9 -0
- package/app/api/reader/flag/route.ts +9 -0
- package/app/api/reader/handle/route.ts +9 -0
- package/app/api/reader/me/route.ts +9 -0
- package/app/api/reader/rating/route.ts +9 -0
- package/app/api/reader/report/route.ts +9 -0
- package/app/api/reader/retired/route.ts +9 -0
- package/app/api/reader/submit/route.ts +9 -0
- package/app/api/reader/vote/route.ts +9 -0
- package/bin/papyrus-infra.mjs +20 -1
- package/components/editorial-checks-admin-panel.tsx +36 -11
- package/components/moderation-panel.tsx +248 -0
- package/components/monitoring-overview-view.tsx +241 -0
- package/components/monitoring-panel.tsx +40 -0
- package/components/newsletter-panel.tsx +474 -0
- package/components/newsletter-sections.tsx +163 -0
- package/components/newsroom-article-editor.tsx +2 -4
- package/components/newsroom-curation-bootstrap.tsx +45 -0
- package/components/newsroom-organizer.css +31 -0
- package/components/newsroom-references-view.tsx +14 -14
- package/components/publication-cyclotron-panel.tsx +15 -10
- package/components/reference-add-entry.tsx +75 -0
- package/components/reference-curation-controls.tsx +31 -22
- package/components/reference-cyclotron-priming.tsx +58 -19
- package/components/reference-cyclotron-suggestion.tsx +1 -1
- package/components/reference-priming.tsx +14 -10
- package/components/reference-section-suggestion.tsx +1 -1
- package/components/topic-steering-workspace.tsx +64 -20
- package/components/use-editorial-checks.ts +12 -10
- package/infra/amplify-app-shell.js +90 -16
- package/infra/amplify-app-shell.ts +102 -18
- package/infra/index.js +1 -0
- package/infra/secure-string-parameter-handler.js +41 -0
- package/infra/secure-string-parameter-handler.ts +41 -0
- package/infra/secure-string-parameter.js +79 -0
- package/infra/secure-string-parameter.ts +91 -0
- package/infra/site-config.js +86 -6
- package/infra/site-config.ts +129 -6
- package/lib/category-repository.ts +11 -9
- package/lib/curation-routes.ts +345 -0
- package/lib/editorial-checks-backend.ts +1 -1
- package/lib/editorial-checks-client.ts +19 -1
- package/lib/editorial-checks-demo.ts +2 -2
- package/lib/editorial-checks-settings.ts +3 -0
- package/lib/editorial-checks.ts +8 -11
- package/lib/editorial-preview.ts +5 -5
- package/lib/monitoring-client.ts +29 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +145 -0
- package/lib/newsletter-api.ts +418 -0
- package/lib/newsletter-client.ts +46 -0
- package/lib/newsletter-fixture.ts +184 -0
- package/lib/newsletter-model.ts +202 -0
- package/lib/prospectus-session.ts +15 -14
- package/lib/prospectus-sign-in-routes.ts +84 -19
- package/lib/prospectus-site-identity.ts +24 -1
- package/lib/reader-api.ts +145 -0
- package/lib/reader-moderation.ts +220 -0
- package/lib/reader-retirement-route.ts +89 -0
- package/lib/reader-subject.md +29 -0
- package/lib/reader-subject.ts +53 -0
- package/lib/relevance-status.ts +2 -2
- package/lib/semantic-graph.ts +2 -0
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +6 -3
- package/lib/site-origin.ts +67 -0
- package/middleware.ts +14 -3
- package/package.json +42 -3
- package/routes.manifest.json +215 -2
- package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
- package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -69
- package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -85
- package/amplify/plugins/editorial-checks/schema.js +0 -94
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
2
|
+
import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
3
|
+
import { SiteOriginNotConfiguredError, publicSiteOrigin, siteOriginNotConfiguredResponse } from "./site-origin";
|
|
4
|
+
import { resolveProspectusSignInRuntime, type ProspectusSignInRuntime } from "./prospectus-session";
|
|
5
|
+
import { ProspectusAccountsError } from "./prospectus-site-identity";
|
|
6
|
+
import { secretsMatch } from "./revalidate-secret";
|
|
7
|
+
|
|
8
|
+
export const READER_POLL_SECRET_PARAMETER_ENV = "PAPYRUS_READER_POLL_SECRET_PARAMETER";
|
|
9
|
+
export const READER_POLL_SECRET_HEADER = "x-papyrus-reader-poll-secret";
|
|
10
|
+
export const READER_POLL_SECRET_CACHE_MILLISECONDS = 5 * 60 * 1000;
|
|
11
|
+
const CURSOR_PATTERN = /^[A-Za-z0-9._~=+/-]{1,512}$/;
|
|
12
|
+
const NO_STORE_HEADERS = { "cache-control": "no-store" };
|
|
13
|
+
|
|
14
|
+
export interface ReaderRetirementRouteOptions {
|
|
15
|
+
environment?: Record<string, string | undefined>;
|
|
16
|
+
readSecret?: (parameterName: string) => Promise<string | null>;
|
|
17
|
+
resolveRuntime?: (siteOrigin: string) => Promise<Pick<ProspectusSignInRuntime, "accounts"> | null>;
|
|
18
|
+
now?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type CachedSecret = { parameterName: string; value: string; expiresAt: number };
|
|
22
|
+
let cachedSecret: CachedSecret | null = null;
|
|
23
|
+
let defaultSsmClient: SSMClient | null = null;
|
|
24
|
+
|
|
25
|
+
export function resetReaderPollSecretCache(): void {
|
|
26
|
+
cachedSecret = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readSecretFromSsm(parameterName: string): Promise<string | null> {
|
|
30
|
+
defaultSsmClient ??= new SSMClient({});
|
|
31
|
+
const response = await defaultSsmClient.send(new GetParameterCommand({ Name: parameterName, WithDecryption: true }));
|
|
32
|
+
return response.Parameter?.Value?.trim() || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function loadReaderPollSecret(options: ReaderRetirementRouteOptions, environment: Record<string, string | undefined>): Promise<string | null> {
|
|
36
|
+
const parameterName = environment[READER_POLL_SECRET_PARAMETER_ENV]?.trim();
|
|
37
|
+
if (!parameterName) return null;
|
|
38
|
+
const now = options.now ?? Date.now();
|
|
39
|
+
if (cachedSecret && cachedSecret.parameterName === parameterName && cachedSecret.expiresAt > now) return cachedSecret.value;
|
|
40
|
+
try {
|
|
41
|
+
const value = (await (options.readSecret ?? readSecretFromSsm)(parameterName))?.trim();
|
|
42
|
+
if (!value) return null;
|
|
43
|
+
cachedSecret = { parameterName, value, expiresAt: now + READER_POLL_SECRET_CACHE_MILLISECONDS };
|
|
44
|
+
return value;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`Reader poll secret could not be read (${error instanceof Error ? error.name : "error"}).`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function failure(status: number, error: string, message: string): NextResponse {
|
|
52
|
+
return NextResponse.json({ error, message }, { status, headers: NO_STORE_HEADERS });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The scheduled snapshot run's way to Prospectus's retired-subject route. Prospectus admits one caller per site, this
|
|
57
|
+
* app's compute role, so the run cannot call it directly: it calls this route with a shared secret from SSM, and the route
|
|
58
|
+
* calls Prospectus as the compute role and passes the answer through. Anyone without the secret gets 401, and a site
|
|
59
|
+
* without the secret parameter gets 503, so this is never open.
|
|
60
|
+
*/
|
|
61
|
+
export async function handleReaderRetiredSubjects(request: NextRequest, options: ReaderRetirementRouteOptions = {}): Promise<NextResponse> {
|
|
62
|
+
const environment = options.environment ?? process.env;
|
|
63
|
+
const expected = await loadReaderPollSecret(options, environment);
|
|
64
|
+
if (!expected) return failure(503, "reader_poll_unavailable", "The retirement poll is not configured for this site.");
|
|
65
|
+
if (!secretsMatch(request.headers.get(READER_POLL_SECRET_HEADER), expected)) return failure(401, "unauthorized", "Missing or wrong poll secret.");
|
|
66
|
+
|
|
67
|
+
const since = request.nextUrl.searchParams.get("since") ?? undefined;
|
|
68
|
+
const unknownParameters = [...request.nextUrl.searchParams.keys()].filter((key) => key !== "since");
|
|
69
|
+
if (unknownParameters.length > 0 || (since !== undefined && !CURSOR_PATTERN.test(since))) {
|
|
70
|
+
return failure(400, "invalid_since", "Send only a since cursor.");
|
|
71
|
+
}
|
|
72
|
+
let siteOrigin: string;
|
|
73
|
+
try {
|
|
74
|
+
siteOrigin = publicSiteOrigin(request, environment);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof SiteOriginNotConfiguredError) return siteOriginNotConfiguredResponse(error);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
const runtime = await (options.resolveRuntime ?? ((origin) => resolveProspectusSignInRuntime(origin, { environment })))(siteOrigin);
|
|
80
|
+
if (!runtime) return failure(503, "prospectus_sign_in_unavailable", "Prospectus is not configured for this site.");
|
|
81
|
+
try {
|
|
82
|
+
const page = await runtime.accounts.retiredSiteSubjects(since);
|
|
83
|
+
return NextResponse.json({ retired: page.retired, cursor: page.cursor, more: page.more }, { headers: NO_STORE_HEADERS });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof ProspectusAccountsError) return failure(error.status === 400 ? 400 : 502, error.code, error.message);
|
|
86
|
+
console.error(`Prospectus retired subjects failed (${error instanceof Error ? error.name : "error"}).`);
|
|
87
|
+
return failure(502, "prospectus_unavailable", "Prospectus could not be reached.");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Reader subject
|
|
2
|
+
|
|
3
|
+
`readerSubject(session)` in `reader-subject.ts` is the only place a reader's identity for the reader API is
|
|
4
|
+
decided. It returns the per-site pairwise `siteSubject` (`ss1_` plus 43 base64url characters) that Prospectus
|
|
5
|
+
puts on `person.siteSubject`, or `null`.
|
|
6
|
+
|
|
7
|
+
## Production
|
|
8
|
+
|
|
9
|
+
Only the `siteSubject` the sealed session cookie carries counts. While Prospectus does not return one, the
|
|
10
|
+
accessor returns `null`, every reader route answers `503 reader_identity_unavailable`, and the reader features
|
|
11
|
+
stay off. There is no other flag. The global Prospectus person id is never used as a reader key.
|
|
12
|
+
|
|
13
|
+
## Development and test only
|
|
14
|
+
|
|
15
|
+
When `SITE_ENV` is `development` or `test`, `NODE_ENV` is not `production` and `PAPYRUS_READER_DEV_SECRET`
|
|
16
|
+
holds at least 16 characters, a session without a `siteSubject` gets the interim value
|
|
17
|
+
`ssdev_` plus `base64url(HMAC-SHA-256(devSecret, personId))`.
|
|
18
|
+
|
|
19
|
+
That path never runs in production. No remap from `ssdev_` values to `ss1_` values is ever needed: production
|
|
20
|
+
reader writes stay closed until `siteSubject` is live, so no production row is ever keyed on an interim value.
|
|
21
|
+
The `reader-actions` function also refuses `ssdev_` subjects unless the site was synthesized with a
|
|
22
|
+
development or test `SITE_ENV`.
|
|
23
|
+
|
|
24
|
+
## Staff
|
|
25
|
+
|
|
26
|
+
`readerIsStaff(session)` reads the `cognito:groups` claim from the access token inside the sealed session
|
|
27
|
+
cookie (`editor` or `admin`). The cookie is written by the site's own callback route, so the token is trusted
|
|
28
|
+
the way the rest of the session is. The reader API passes the result to the function as `isStaff`; clients never
|
|
29
|
+
send it.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import type { ProspectusSignedInSession } from "./prospectus-session";
|
|
3
|
+
|
|
4
|
+
export const SITE_SUBJECT_PATTERN = /^ss1_[A-Za-z0-9_-]{43}$/;
|
|
5
|
+
export const INTERIM_SUBJECT_PATTERN = /^ssdev_[A-Za-z0-9_-]{43}$/;
|
|
6
|
+
export const READER_DEV_SECRET_ENV = "PAPYRUS_READER_DEV_SECRET";
|
|
7
|
+
export const READER_STAFF_GROUPS = ["editor", "admin"] as const;
|
|
8
|
+
|
|
9
|
+
const MINIMUM_DEV_SECRET_LENGTH = 16;
|
|
10
|
+
const INTERIM_SUBJECT_PREFIX = "ssdev_";
|
|
11
|
+
const INTERIM_ENVIRONMENTS = ["development", "test"];
|
|
12
|
+
|
|
13
|
+
type EnvironmentVariables = Record<string, string | undefined>;
|
|
14
|
+
|
|
15
|
+
export function interimSubjectsAllowed(environment: EnvironmentVariables = process.env): boolean {
|
|
16
|
+
if (environment.NODE_ENV === "production") return false;
|
|
17
|
+
const siteEnvironment = (environment.SITE_ENV ?? "").trim();
|
|
18
|
+
if (!INTERIM_ENVIRONMENTS.includes(siteEnvironment)) return false;
|
|
19
|
+
return (environment[READER_DEV_SECRET_ENV] ?? "").length >= MINIMUM_DEV_SECRET_LENGTH;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function interimSubject(personId: string, devSecret: string): string {
|
|
23
|
+
return INTERIM_SUBJECT_PREFIX + createHmac("sha256", devSecret).update(personId).digest("base64url");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The reader's per-site pairwise id, or null. Production reads only the siteSubject Prospectus returned;
|
|
28
|
+
* the interim `ssdev_` value exists in development and test only (lib/reader-subject.md).
|
|
29
|
+
*/
|
|
30
|
+
export function readerSubject(
|
|
31
|
+
session: Pick<ProspectusSignedInSession, "personId" | "siteSubject"> | null | undefined,
|
|
32
|
+
environment: EnvironmentVariables = process.env,
|
|
33
|
+
): string | null {
|
|
34
|
+
if (!session) return null;
|
|
35
|
+
if (typeof session.siteSubject === "string" && SITE_SUBJECT_PATTERN.test(session.siteSubject)) return session.siteSubject;
|
|
36
|
+
if (!interimSubjectsAllowed(environment) || !session.personId) return null;
|
|
37
|
+
return interimSubject(session.personId, environment[READER_DEV_SECRET_ENV] as string);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Staff membership from the Cognito groups claim of the access token that the sealed session cookie carries. */
|
|
41
|
+
export function readerIsStaff(session: Pick<ProspectusSignedInSession, "accessToken"> | null | undefined): boolean {
|
|
42
|
+
const token = session?.accessToken;
|
|
43
|
+
if (typeof token !== "string") return false;
|
|
44
|
+
const payload = token.split(".")[1];
|
|
45
|
+
if (!payload) return false;
|
|
46
|
+
try {
|
|
47
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as { "cognito:groups"?: unknown };
|
|
48
|
+
const groups = claims["cognito:groups"];
|
|
49
|
+
return Array.isArray(groups) && groups.some((group) => (READER_STAFF_GROUPS as readonly unknown[]).includes(group));
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/lib/relevance-status.ts
CHANGED
|
@@ -52,10 +52,10 @@ export const DEMO_CYCLOTRON_STATUS: CyclotronStatus = {
|
|
|
52
52
|
},
|
|
53
53
|
calibration: { saysSure: null, isRight: null, gapPoints: null, curve: [] },
|
|
54
54
|
reviewRate: {
|
|
55
|
-
state: "onboarding", rate: 1, reason: "
|
|
55
|
+
state: "onboarding", rate: 1, reason: "Getting started: every suggestion is reviewed until suggestions earn a lower review rate.",
|
|
56
56
|
auditFloor: 0.05, confidenceThreshold: 0.7, override: null,
|
|
57
57
|
nextCheckAfterDecisions: 99,
|
|
58
|
-
nextStep: "Steps down to 50% after 2 more windows of 100 decisions with accuracy on confident decisions of at least 85% (at least 20 reviewed) and a
|
|
58
|
+
nextStep: "Steps down to 50% after 2 more windows of 100 decisions with accuracy on confident decisions of at least 85% (at least 20 reviewed) and a gap under 5 points between how sure suggestions say they are and how often they are right. Next check in 99 decisions.",
|
|
59
59
|
expectedReviewsPerWeek: 1,
|
|
60
60
|
},
|
|
61
61
|
lastChange: null,
|
package/lib/semantic-graph.ts
CHANGED
|
@@ -57,6 +57,7 @@ export type SemanticPredicateId =
|
|
|
57
57
|
| "related_to"
|
|
58
58
|
| "broader_than"
|
|
59
59
|
| "narrower_than"
|
|
60
|
+
| "member_of"
|
|
60
61
|
| "supports"
|
|
61
62
|
| "contradicts";
|
|
62
63
|
|
|
@@ -96,6 +97,7 @@ export const SEMANTIC_PREDICATES: SemanticPredicateDefinition[] = [
|
|
|
96
97
|
{ id: "related_to", label: "related to", group: "generic", inverseLabel: "related from" },
|
|
97
98
|
{ id: "broader_than", label: "broader than", group: "ontology", inverseLabel: "narrower concept" },
|
|
98
99
|
{ id: "narrower_than", label: "narrower than", group: "ontology", inverseLabel: "broader concept" },
|
|
100
|
+
{ id: "member_of", label: "member of", group: "ontology", inverseLabel: "members" },
|
|
99
101
|
{ id: "supports", label: "supports", group: "evidence", inverseLabel: "supported by" },
|
|
100
102
|
{ id: "contradicts", label: "contradicts", group: "evidence", inverseLabel: "contradicted by" },
|
|
101
103
|
];
|
package/lib/site-brand-tenant.ts
CHANGED
|
@@ -9,14 +9,19 @@ export function resolveBrandConfigPath(relativePath: string): string {
|
|
|
9
9
|
return path.isAbsolute(relativePath) ? relativePath : path.join(process.cwd(), relativePath);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/** Optional repo files a brand may still name; undefined means the site keeps that configuration as backend data and nothing is looked up. */
|
|
13
|
+
function optionalBrandConfigPath(relativePath: string | undefined): string | undefined {
|
|
14
|
+
return relativePath ? resolveBrandConfigPath(relativePath) : undefined;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function
|
|
17
|
-
return
|
|
17
|
+
export function getBrandSteeringConfigPath(brand: SiteBrand = resolveActiveSiteBrand()): string | undefined {
|
|
18
|
+
return optionalBrandConfigPath(brand.steeringConfigPath);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export function
|
|
21
|
-
return
|
|
21
|
+
export function getBrandNewsroomSectionsConfigPath(brand: SiteBrand = resolveActiveSiteBrand()): string | undefined {
|
|
22
|
+
return optionalBrandConfigPath(brand.newsroomSectionsConfigPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getBrandAnalysisProfilesPath(brand: SiteBrand = resolveActiveSiteBrand()): string | undefined {
|
|
26
|
+
return optionalBrandConfigPath(brand.analysisProfilesPath);
|
|
22
27
|
}
|
package/lib/site-brand.ts
CHANGED
|
@@ -142,9 +142,12 @@ export type SiteBrand = {
|
|
|
142
142
|
readerBasePath?: string;
|
|
143
143
|
opsChrome: OpsChrome;
|
|
144
144
|
corpusKey: string;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
/** Optional repo file for topic steering. Editorial and steering configuration is backend data; a data-driven site sets none and nothing is looked up. */
|
|
146
|
+
steeringConfigPath?: string;
|
|
147
|
+
/** Optional repo file for the local-preview demo sections only. Sections live in the backend. */
|
|
148
|
+
newsroomSectionsConfigPath?: string;
|
|
149
|
+
/** Optional repo file listing analysis profiles. */
|
|
150
|
+
analysisProfilesPath?: string;
|
|
148
151
|
publicationName: string;
|
|
149
152
|
components?: SiteBrandComponents;
|
|
150
153
|
/**
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
2
|
+
import { getSiteEnv } from "./site-env";
|
|
3
|
+
|
|
4
|
+
type EnvironmentVariables = Record<string, string | undefined>;
|
|
5
|
+
|
|
6
|
+
export const SITE_ORIGIN_ENV = "PAPYRUS_SITE_ORIGIN";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The site's public origin cannot be read from the request on Amplify SSR (WEB_COMPUTE):
|
|
10
|
+
* there `request.nextUrl.origin` is the compute adapter's internal https://localhost:3000.
|
|
11
|
+
* It also must never be derived from Host or X-Forwarded-* headers, which a client can set.
|
|
12
|
+
* The one trusted source is PAPYRUS_SITE_ORIGIN, set by the app shell from the site's host.
|
|
13
|
+
*/
|
|
14
|
+
export class SiteOriginNotConfiguredError extends Error {
|
|
15
|
+
readonly status = 503;
|
|
16
|
+
readonly code = "site_origin_not_configured";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function siteOriginNotConfiguredResponse(error: SiteOriginNotConfiguredError): NextResponse {
|
|
20
|
+
return NextResponse.json({ error: error.code, message: error.message }, { status: error.status, headers: { "cache-control": "no-store" } });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
24
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function configuredOrigin(value: string): string {
|
|
28
|
+
const invalid = () => new SiteOriginNotConfiguredError(`${SITE_ORIGIN_ENV} must be an https origin such as https://example.com with no path.`);
|
|
29
|
+
let parsed: URL;
|
|
30
|
+
try {
|
|
31
|
+
parsed = new URL(value);
|
|
32
|
+
} catch {
|
|
33
|
+
throw invalid();
|
|
34
|
+
}
|
|
35
|
+
if (parsed.protocol !== "https:" || parsed.origin !== value) throw invalid();
|
|
36
|
+
return parsed.origin;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function siteEnvOrNull(environment: EnvironmentVariables): string | null {
|
|
40
|
+
try {
|
|
41
|
+
return getSiteEnv(environment);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The site's public origin, from trusted configuration.
|
|
49
|
+
* - PAPYRUS_SITE_ORIGIN set: it must be an https origin with no path; it is used as is.
|
|
50
|
+
* - Unset: `request.nextUrl.origin` only when it is loopback, SITE_ENV resolves to development
|
|
51
|
+
* and NODE_ENV is not production (a local `next dev` server). Anything else, production and staging included, throws
|
|
52
|
+
* SiteOriginNotConfiguredError so routes answer 503 instead of redirecting to localhost.
|
|
53
|
+
*/
|
|
54
|
+
export function publicSiteOrigin(request: Pick<NextRequest, "nextUrl">, environment: EnvironmentVariables = process.env): string {
|
|
55
|
+
const configured = (environment[SITE_ORIGIN_ENV] ?? "").trim();
|
|
56
|
+
if (configured) return configuredOrigin(configured);
|
|
57
|
+
const { hostname, origin } = request.nextUrl;
|
|
58
|
+
// SITE_ENV defaults to development when a runtime never received it, so NODE_ENV=production also closes the fallback.
|
|
59
|
+
const developing = siteEnvOrNull(environment) === "development" && environment.NODE_ENV !== "production";
|
|
60
|
+
if (isLoopbackHostname(hostname) && developing) return origin;
|
|
61
|
+
throw new SiteOriginNotConfiguredError(`${SITE_ORIGIN_ENV} is not set; this site cannot tell its public origin.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** An absolute URL on the site's public origin, for redirects. Fails closed like publicSiteOrigin. */
|
|
65
|
+
export function publicSiteUrl(path: string, request: Pick<NextRequest, "nextUrl">, environment: EnvironmentVariables = process.env): URL {
|
|
66
|
+
return new URL(path, publicSiteOrigin(request, environment));
|
|
67
|
+
}
|
package/middleware.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { newsroomHref } from "./lib/newsroom-base-path";
|
|
|
5
5
|
import { getReaderBasePath } from "./lib/reader-base-path";
|
|
6
6
|
import { isStagingGatedPath, isStaticOrApiPath } from "./lib/staging-gated-path";
|
|
7
7
|
import { getSiteEnv, isIndexable } from "./lib/site-env";
|
|
8
|
+
import { SiteOriginNotConfiguredError, publicSiteUrl, siteOriginNotConfiguredResponse } from "./lib/site-origin";
|
|
8
9
|
import { getStagingAccess } from "./lib/staging-gate";
|
|
9
10
|
import {
|
|
10
11
|
isPreviewGatedPath,
|
|
@@ -38,11 +39,11 @@ function routeRequest(request: NextRequest): NextResponse {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
if (pathname === "/newsroom" || pathname === "/newsroom/") {
|
|
41
|
-
return NextResponse.redirect(
|
|
42
|
+
return NextResponse.redirect(publicSiteUrl(`/${request.nextUrl.search}`, request));
|
|
42
43
|
}
|
|
43
44
|
if (pathname.startsWith("/newsroom/")) {
|
|
44
45
|
const target = pathname.slice("/newsroom".length) || "/";
|
|
45
|
-
return NextResponse.redirect(
|
|
46
|
+
return NextResponse.redirect(publicSiteUrl(`${target}${request.nextUrl.search}`, request));
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
if (pathname === "/" || pathname === "") {
|
|
@@ -59,6 +60,16 @@ function routeRequest(request: NextRequest): NextResponse {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
export async function middleware(request: NextRequest): Promise<NextResponse> {
|
|
63
|
+
try {
|
|
64
|
+
return await handleRequest(request);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Redirects name the site's public origin (request.url is localhost on Amplify); without it, fail closed.
|
|
67
|
+
if (error instanceof SiteOriginNotConfiguredError) return withRobotsHeader(siteOriginNotConfiguredResponse(error));
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function handleRequest(request: NextRequest): Promise<NextResponse> {
|
|
62
73
|
let sessionResponse: NextResponse | null = null;
|
|
63
74
|
const { pathname } = request.nextUrl;
|
|
64
75
|
const gated = isStaticPreviewEnabled()
|
|
@@ -68,7 +79,7 @@ export async function middleware(request: NextRequest): Promise<NextResponse> {
|
|
|
68
79
|
sessionResponse = NextResponse.next();
|
|
69
80
|
const access = await getStagingAccess(request, sessionResponse);
|
|
70
81
|
if (access === "anonymous") {
|
|
71
|
-
return withRobotsHeader(NextResponse.redirect(
|
|
82
|
+
return withRobotsHeader(NextResponse.redirect(publicSiteUrl(newsroomHref(), request)));
|
|
72
83
|
}
|
|
73
84
|
if (access === "forbidden") {
|
|
74
85
|
return withRobotsHeader(new NextResponse("Staging is limited to editors and admins.", { status: 403 }));
|
package/package.json
CHANGED
|
@@ -85,9 +85,14 @@
|
|
|
85
85
|
"./components/item-stream-view": "./components/item-stream-view.tsx",
|
|
86
86
|
"./components/markus-body-editor": "./components/markus-body-editor.tsx",
|
|
87
87
|
"./components/markus-ir-preview": "./components/markus-ir-preview.tsx",
|
|
88
|
+
"./components/moderation-panel": "./components/moderation-panel.tsx",
|
|
89
|
+
"./components/monitoring-overview-view": "./components/monitoring-overview-view.tsx",
|
|
90
|
+
"./components/monitoring-panel": "./components/monitoring-panel.tsx",
|
|
88
91
|
"./components/news-desk-client-provider": "./components/news-desk-client-provider.tsx",
|
|
89
92
|
"./components/news-desk-page": "./components/news-desk-page.tsx",
|
|
90
93
|
"./components/news-desk-taxonomy-client": "./components/news-desk-taxonomy-client.ts",
|
|
94
|
+
"./components/newsletter-panel": "./components/newsletter-panel.tsx",
|
|
95
|
+
"./components/newsletter-sections": "./components/newsletter-sections.tsx",
|
|
91
96
|
"./components/newspaper": "./components/newspaper.tsx",
|
|
92
97
|
"./components/newsroom-access-gate": "./components/newsroom-access-gate.tsx",
|
|
93
98
|
"./components/newsroom-article-editor": "./components/newsroom-article-editor.tsx",
|
|
@@ -96,6 +101,7 @@
|
|
|
96
101
|
"./components/newsroom-assignments-view": "./components/newsroom-assignments-view.tsx",
|
|
97
102
|
"./components/newsroom-board-view": "./components/newsroom-board-view.tsx",
|
|
98
103
|
"./components/newsroom-client-shell": "./components/newsroom-client-shell.tsx",
|
|
104
|
+
"./components/newsroom-curation-bootstrap": "./components/newsroom-curation-bootstrap.tsx",
|
|
99
105
|
"./components/newsroom-curation-provider": "./components/newsroom-curation-provider.tsx",
|
|
100
106
|
"./components/newsroom-ops-overview": "./components/newsroom-ops-overview.tsx",
|
|
101
107
|
"./components/newsroom-ops-shell": "./components/newsroom-ops-shell.tsx",
|
|
@@ -118,6 +124,7 @@
|
|
|
118
124
|
"./components/reader-auth-state": "./components/reader-auth-state.ts",
|
|
119
125
|
"./components/reader-settings": "./components/reader-settings.ts",
|
|
120
126
|
"./components/reader-theme-control": "./components/reader-theme-control.tsx",
|
|
127
|
+
"./components/reference-add-entry": "./components/reference-add-entry.tsx",
|
|
121
128
|
"./components/reference-curation-controls": "./components/reference-curation-controls.tsx",
|
|
122
129
|
"./components/reference-cyclotron-priming": "./components/reference-cyclotron-priming.tsx",
|
|
123
130
|
"./components/reference-cyclotron-status": "./components/reference-cyclotron-status.tsx",
|
|
@@ -188,6 +195,7 @@
|
|
|
188
195
|
"./lib/content-types": "./lib/content-types.ts",
|
|
189
196
|
"./lib/curation-client": "./lib/curation-client.ts",
|
|
190
197
|
"./lib/curation-delegation-helpers": "./lib/curation-delegation-helpers.ts",
|
|
198
|
+
"./lib/curation-routes": "./lib/curation-routes.ts",
|
|
191
199
|
"./lib/cyclotron-suggestions": "./lib/cyclotron-suggestions.ts",
|
|
192
200
|
"./lib/define-site": "./lib/define-site.ts",
|
|
193
201
|
"./lib/demo-amplify-outputs": "./lib/demo-amplify-outputs.ts",
|
|
@@ -224,7 +232,14 @@
|
|
|
224
232
|
"./lib/markus-projection": "./lib/markus-projection.ts",
|
|
225
233
|
"./lib/markus-to-article": "./lib/markus-to-article.ts",
|
|
226
234
|
"./lib/media-route-response": "./lib/media-route-response.ts",
|
|
235
|
+
"./lib/monitoring-client": "./lib/monitoring-client.ts",
|
|
236
|
+
"./lib/monitoring-overview-fixture": "./lib/monitoring-overview-fixture.ts",
|
|
237
|
+
"./lib/monitoring-overview": "./lib/monitoring-overview.ts",
|
|
227
238
|
"./lib/news-desk-session": "./lib/news-desk-session.ts",
|
|
239
|
+
"./lib/newsletter-api": "./lib/newsletter-api.ts",
|
|
240
|
+
"./lib/newsletter-client": "./lib/newsletter-client.ts",
|
|
241
|
+
"./lib/newsletter-fixture": "./lib/newsletter-fixture.ts",
|
|
242
|
+
"./lib/newsletter-model": "./lib/newsletter-model.ts",
|
|
228
243
|
"./lib/newspaper-layout": "./lib/newspaper-layout.ts",
|
|
229
244
|
"./lib/newsroom-articles": "./lib/newsroom-articles.ts",
|
|
230
245
|
"./lib/newsroom-assignments": "./lib/newsroom-assignments.ts",
|
|
@@ -265,11 +280,15 @@
|
|
|
265
280
|
"./lib/publication-cyclotron": "./lib/publication-cyclotron.ts",
|
|
266
281
|
"./lib/publication-items": "./lib/publication-items.ts",
|
|
267
282
|
"./lib/rate-limit-retry": "./lib/rate-limit-retry.ts",
|
|
283
|
+
"./lib/reader-api": "./lib/reader-api.ts",
|
|
268
284
|
"./lib/reader-base-path": "./lib/reader-base-path.ts",
|
|
269
285
|
"./lib/reader-cache-lifetime": "./lib/reader-cache-lifetime.ts",
|
|
286
|
+
"./lib/reader-moderation": "./lib/reader-moderation.ts",
|
|
287
|
+
"./lib/reader-retirement-route": "./lib/reader-retirement-route.ts",
|
|
270
288
|
"./lib/reader-route-config": "./lib/reader-route-config.ts",
|
|
271
289
|
"./lib/reader-static-params": "./lib/reader-static-params.ts",
|
|
272
290
|
"./lib/reader-storage-url": "./lib/reader-storage-url.ts",
|
|
291
|
+
"./lib/reader-subject": "./lib/reader-subject.ts",
|
|
273
292
|
"./lib/reference-display": "./lib/reference-display.ts",
|
|
274
293
|
"./lib/reference-policy": "./lib/reference-policy.ts",
|
|
275
294
|
"./lib/reference-source-preview": "./lib/reference-source-preview.ts",
|
|
@@ -288,6 +307,7 @@
|
|
|
288
307
|
"./lib/site-brand-tenant": "./lib/site-brand-tenant.ts",
|
|
289
308
|
"./lib/site-brand": "./lib/site-brand.ts",
|
|
290
309
|
"./lib/site-env": "./lib/site-env.ts",
|
|
310
|
+
"./lib/site-origin": "./lib/site-origin.ts",
|
|
291
311
|
"./lib/site-renderer": "./lib/site-renderer.ts",
|
|
292
312
|
"./lib/site-section-suggestions": "./lib/site-section-suggestions.ts",
|
|
293
313
|
"./lib/site-stack": "./lib/site-stack.ts",
|
|
@@ -330,6 +350,25 @@
|
|
|
330
350
|
"./app/api/local-preview/derive/route": "./app/api/local-preview/derive/route.ts",
|
|
331
351
|
"./app/api/local-preview/media/route": "./app/api/local-preview/media/route.ts",
|
|
332
352
|
"./app/api/media/[...path]/route": "./app/api/media/[...path]/route.ts",
|
|
353
|
+
"./app/api/newsletter/confirm/route": "./app/api/newsletter/confirm/route.ts",
|
|
354
|
+
"./app/api/newsletter/preferences/route": "./app/api/newsletter/preferences/route.ts",
|
|
355
|
+
"./app/api/newsletter/state/route": "./app/api/newsletter/state/route.ts",
|
|
356
|
+
"./app/api/newsletter/subscribe/route": "./app/api/newsletter/subscribe/route.ts",
|
|
357
|
+
"./app/api/newsletter/unsubscribe/route": "./app/api/newsletter/unsubscribe/route.ts",
|
|
358
|
+
"./app/api/newsroom/curation/context/route": "./app/api/newsroom/curation/context/route.ts",
|
|
359
|
+
"./app/api/newsroom/curation/priming/route": "./app/api/newsroom/curation/priming/route.ts",
|
|
360
|
+
"./app/api/newsroom/curation/publication/route": "./app/api/newsroom/curation/publication/route.ts",
|
|
361
|
+
"./app/api/reader/comment/delete/route": "./app/api/reader/comment/delete/route.ts",
|
|
362
|
+
"./app/api/reader/comment/edit/route": "./app/api/reader/comment/edit/route.ts",
|
|
363
|
+
"./app/api/reader/comment/route": "./app/api/reader/comment/route.ts",
|
|
364
|
+
"./app/api/reader/flag/route": "./app/api/reader/flag/route.ts",
|
|
365
|
+
"./app/api/reader/handle/route": "./app/api/reader/handle/route.ts",
|
|
366
|
+
"./app/api/reader/me/route": "./app/api/reader/me/route.ts",
|
|
367
|
+
"./app/api/reader/rating/route": "./app/api/reader/rating/route.ts",
|
|
368
|
+
"./app/api/reader/report/route": "./app/api/reader/report/route.ts",
|
|
369
|
+
"./app/api/reader/retired/route": "./app/api/reader/retired/route.ts",
|
|
370
|
+
"./app/api/reader/submit/route": "./app/api/reader/submit/route.ts",
|
|
371
|
+
"./app/api/reader/vote/route": "./app/api/reader/vote/route.ts",
|
|
333
372
|
"./app/api/revalidate/route": "./app/api/revalidate/route.ts",
|
|
334
373
|
"./app/archive/page": "./app/archive/page.tsx",
|
|
335
374
|
"./app/articles/[slug]/page": "./app/articles/[slug]/page.tsx",
|
|
@@ -395,14 +434,14 @@
|
|
|
395
434
|
"optional": true
|
|
396
435
|
}
|
|
397
436
|
},
|
|
398
|
-
"version": "2.0.0-next.
|
|
437
|
+
"version": "2.0.0-next.70",
|
|
399
438
|
"dependencies": {
|
|
400
439
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
401
440
|
"@aws-crypto/sha256-js": "5.2.0",
|
|
402
441
|
"@aws-sdk/client-cognito-identity-provider": "3.1045.0",
|
|
442
|
+
"@aws-sdk/client-dynamodb": "^3.1045.0",
|
|
403
443
|
"@aws-sdk/client-lambda": "^3.1045.0",
|
|
404
444
|
"@aws-sdk/client-s3": "^3.1045.0",
|
|
405
|
-
"@aws-sdk/client-secrets-manager": "3.1045.0",
|
|
406
445
|
"@aws-sdk/client-ssm": "^3.1045.0",
|
|
407
446
|
"@aws-sdk/credential-provider-node": "3.972.84",
|
|
408
447
|
"@aws-sdk/s3-request-presigner": "^3.1045.0",
|
|
@@ -435,7 +474,7 @@
|
|
|
435
474
|
"class-variance-authority": "^0.7.1",
|
|
436
475
|
"clsx": "^2.1.1",
|
|
437
476
|
"cmdk": "^1.1.1",
|
|
438
|
-
"cyclotron": "https://github.com/AnthusAI/Cyclotron/releases/download/ui-v0.2.
|
|
477
|
+
"cyclotron": "https://github.com/AnthusAI/Cyclotron/releases/download/ui-v0.2.3/cyclotron-0.2.3.tgz",
|
|
439
478
|
"d3": "^7.9.0",
|
|
440
479
|
"esbuild": "^0.28.2",
|
|
441
480
|
"framer-motion": "^14.3.0",
|