@anthusai/papyrus 5.1.0 → 5.3.0
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/schema.js +2 -0
- package/amplify/data/schema.ts +2 -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/publication/redirect-and-index.js +49 -0
- package/amplify/publication/redirect-rules.js +26 -10
- package/amplify/publication/redirect-rules.ts +36 -10
- package/amplify/publication/s3-projection.js +4 -2
- package/amplify/publication/s3-projection.ts +4 -2
- package/amplify/site-backend.js +4 -0
- package/amplify/site-backend.ts +4 -0
- package/components/monitoring-overview-view.tsx +240 -0
- package/components/monitoring-panel.tsx +40 -0
- package/components/newsroom-organizer.css +17 -0
- package/components/newsroom-references-view.tsx +14 -14
- package/components/publication-cyclotron-panel.tsx +10 -10
- package/components/reference-add-entry.tsx +75 -0
- package/components/reference-curation-controls.tsx +30 -21
- package/components/reference-cyclotron-priming.tsx +4 -4
- 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 +13 -2
- package/lib/category-repository.ts +1 -1
- package/lib/monitoring-client.ts +29 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +144 -0
- package/lib/relevance-status.ts +2 -2
- package/package.json +8 -2
- package/routes.manifest.json +1 -1
package/amplify/data/schema.js
CHANGED
|
@@ -2,6 +2,7 @@ import { a } from "@aws-amplify/backend";
|
|
|
2
2
|
import { assignmentAction } from "../functions/assignment-action/resource.js";
|
|
3
3
|
import { categoryAction } from "../functions/category-action/resource.js";
|
|
4
4
|
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema.js";
|
|
5
|
+
import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema.js";
|
|
5
6
|
import { contentActions } from "../functions/content-actions/resource.js";
|
|
6
7
|
import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
|
|
7
8
|
import { manageUserRole } from "../functions/manage-user-role/resource.js";
|
|
@@ -2001,6 +2002,7 @@ const baseDefinitions = {
|
|
|
2001
2002
|
allow.groups(contentWriteGroups),
|
|
2002
2003
|
]),
|
|
2003
2004
|
...editorialChecksSchemaDefinitions,
|
|
2005
|
+
...monitoringViewSchemaDefinitions,
|
|
2004
2006
|
};
|
|
2005
2007
|
export const schema = a.schema(baseDefinitions).authorization((allow) => [
|
|
2006
2008
|
allow.resource(assignmentAction).to(["query", "mutate"]),
|
package/amplify/data/schema.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ClientSchema, a } from "@aws-amplify/backend";
|
|
|
2
2
|
import { assignmentAction } from "../functions/assignment-action/resource";
|
|
3
3
|
import { categoryAction } from "../functions/category-action/resource";
|
|
4
4
|
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema";
|
|
5
|
+
import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema";
|
|
5
6
|
import { contentActions } from "../functions/content-actions/resource";
|
|
6
7
|
import { knowledgeQuery } from "../functions/knowledge-query/resource";
|
|
7
8
|
import { manageUserRole } from "../functions/manage-user-role/resource";
|
|
@@ -2121,6 +2122,7 @@ const baseDefinitions = {
|
|
|
2121
2122
|
allow.groups(contentWriteGroups),
|
|
2122
2123
|
]),
|
|
2123
2124
|
...editorialChecksSchemaDefinitions,
|
|
2125
|
+
...monitoringViewSchemaDefinitions,
|
|
2124
2126
|
};
|
|
2125
2127
|
|
|
2126
2128
|
export const schema = a.schema(baseDefinitions).authorization((allow) => [
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""The monitoring-view function: `monitoringOverview` for editors and admins, read-only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from papyrus_content.monitoring_view import handle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handler(event: dict[str, Any], _context: Any = None) -> dict[str, Any]:
|
|
11
|
+
return handle(event)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineFunction } from "@aws-amplify/backend";
|
|
2
|
+
import { Duration } from "aws-cdk-lib";
|
|
3
|
+
import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
4
|
+
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
5
|
+
/** AppSync gives a resolver 30 seconds; a read of one small table needs a few. */
|
|
6
|
+
export const MONITORING_VIEW_TIMEOUT_SECONDS = 15;
|
|
7
|
+
/** stdlib plus the runtime's boto3: the bundle is `papyrus_content` and nothing else. */
|
|
8
|
+
export const monitoringView = defineFunction((scope) => new Function(scope, "papyrus-monitoring-view", {
|
|
9
|
+
runtime: Runtime.PYTHON_3_12,
|
|
10
|
+
architecture: Architecture.ARM_64,
|
|
11
|
+
handler: "handler.handler",
|
|
12
|
+
timeout: Duration.seconds(MONITORING_VIEW_TIMEOUT_SECONDS),
|
|
13
|
+
memorySize: 256,
|
|
14
|
+
code: pythonLambdaCode({ functionDir: "monitoring-view", modules: ["papyrus_content"], noDeps: true }, [
|
|
15
|
+
"bash",
|
|
16
|
+
"-c",
|
|
17
|
+
[
|
|
18
|
+
"set -euo pipefail",
|
|
19
|
+
"mkdir -p /asset-output/papyrus_content",
|
|
20
|
+
"cp amplify/functions/monitoring-view/handler.py /asset-output/handler.py",
|
|
21
|
+
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
22
|
+
].join(" && "),
|
|
23
|
+
]),
|
|
24
|
+
}), { resourceGroupName: "data" });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineFunction } from "@aws-amplify/backend";
|
|
2
|
+
import { Duration } from "aws-cdk-lib";
|
|
3
|
+
import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
4
|
+
import type { Construct } from "constructs";
|
|
5
|
+
import { pythonLambdaCode } from "../shared/python-bundle";
|
|
6
|
+
|
|
7
|
+
/** AppSync gives a resolver 30 seconds; a read of one small table needs a few. */
|
|
8
|
+
export const MONITORING_VIEW_TIMEOUT_SECONDS = 15;
|
|
9
|
+
|
|
10
|
+
/** stdlib plus the runtime's boto3: the bundle is `papyrus_content` and nothing else. */
|
|
11
|
+
export const monitoringView = defineFunction(
|
|
12
|
+
(scope: Construct) =>
|
|
13
|
+
new Function(scope, "papyrus-monitoring-view", {
|
|
14
|
+
runtime: Runtime.PYTHON_3_12,
|
|
15
|
+
architecture: Architecture.ARM_64,
|
|
16
|
+
handler: "handler.handler",
|
|
17
|
+
timeout: Duration.seconds(MONITORING_VIEW_TIMEOUT_SECONDS),
|
|
18
|
+
memorySize: 256,
|
|
19
|
+
code: pythonLambdaCode(
|
|
20
|
+
{ functionDir: "monitoring-view", modules: ["papyrus_content"], noDeps: true },
|
|
21
|
+
[
|
|
22
|
+
"bash",
|
|
23
|
+
"-c",
|
|
24
|
+
[
|
|
25
|
+
"set -euo pipefail",
|
|
26
|
+
"mkdir -p /asset-output/papyrus_content",
|
|
27
|
+
"cp amplify/functions/monitoring-view/handler.py /asset-output/handler.py",
|
|
28
|
+
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
29
|
+
].join(" && "),
|
|
30
|
+
],
|
|
31
|
+
),
|
|
32
|
+
}),
|
|
33
|
+
{ resourceGroupName: "data" },
|
|
34
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
import { monitoringView } from "./resource.js";
|
|
3
|
+
const editors = ["editor", "admin"];
|
|
4
|
+
/**
|
|
5
|
+
* Core to every site's data API. The Monitoring page reads the shared monitoring pipeline through
|
|
6
|
+
* this one query. AppSync admits editors and admins only; the function checks the groups again,
|
|
7
|
+
* and returns the whole shared pipeline to admins alone.
|
|
8
|
+
*/
|
|
9
|
+
export const monitoringViewSchemaDefinitions = {
|
|
10
|
+
monitoringOverview: a
|
|
11
|
+
.query()
|
|
12
|
+
.arguments({ scope: a.string() })
|
|
13
|
+
.returns(a.json())
|
|
14
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
15
|
+
.handler(a.handler.function(monitoringView)),
|
|
16
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
import { monitoringView } from "./resource";
|
|
3
|
+
|
|
4
|
+
const editors = ["editor", "admin"];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Core to every site's data API. The Monitoring page reads the shared monitoring pipeline through
|
|
8
|
+
* this one query. AppSync admits editors and admins only; the function checks the groups again,
|
|
9
|
+
* and returns the whole shared pipeline to admins alone.
|
|
10
|
+
*/
|
|
11
|
+
export const monitoringViewSchemaDefinitions = {
|
|
12
|
+
monitoringOverview: a
|
|
13
|
+
.query()
|
|
14
|
+
.arguments({ scope: a.string() })
|
|
15
|
+
.returns(a.json())
|
|
16
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
17
|
+
.handler(a.handler.function(monitoringView)),
|
|
18
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
3
|
+
/** The monitoring stack writes the table's name here (infra/monitoring/papyrus-monitoring.yaml). */
|
|
4
|
+
export const MONITORING_TABLE_PARAMETER = "/papyrus/monitoring/table-name";
|
|
5
|
+
/** Reads only: a single item or a key-range query. No Scan, no writes, no other table. */
|
|
6
|
+
export const MONITORING_VIEW_TABLE_ACTIONS = ["dynamodb:GetItem", "dynamodb:Query"];
|
|
7
|
+
/**
|
|
8
|
+
* The monitoring table is auto-named by CloudFormation (`papyrus-monitoring-MonitoringTable-<id>`), so
|
|
9
|
+
* the grant is the stack's table name prefix, not a wildcard on the account.
|
|
10
|
+
*/
|
|
11
|
+
export function monitoringViewTableArns(region, account) {
|
|
12
|
+
const table = `arn:aws:dynamodb:${region}:${account}:table/papyrus-monitoring-MonitoringTable-*`;
|
|
13
|
+
return [table, `${table}/index/routing`];
|
|
14
|
+
}
|
|
15
|
+
export function monitoringViewParameterArn(region, account) {
|
|
16
|
+
return `arn:aws:ssm:${region}:${account}:parameter${MONITORING_TABLE_PARAMETER}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Environment and least-privilege IAM for the monitoring-view function. A site outside the monitoring
|
|
20
|
+
* account simply finds no parameter and the page says monitoring is not configured.
|
|
21
|
+
*/
|
|
22
|
+
export function wireMonitoringView(backend) {
|
|
23
|
+
const lambda = backend.monitoringView.resources.lambda;
|
|
24
|
+
const stack = Stack.of(lambda);
|
|
25
|
+
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
|
|
26
|
+
lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
|
|
27
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
28
|
+
effect: Effect.ALLOW,
|
|
29
|
+
actions: ["ssm:GetParameter"],
|
|
30
|
+
resources: [monitoringViewParameterArn(stack.region, stack.account)],
|
|
31
|
+
}));
|
|
32
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
33
|
+
effect: Effect.ALLOW,
|
|
34
|
+
actions: MONITORING_VIEW_TABLE_ACTIONS,
|
|
35
|
+
resources: monitoringViewTableArns(stack.region, stack.account),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
3
|
+
import type { Function as LambdaFunction } from "aws-cdk-lib/aws-lambda";
|
|
4
|
+
|
|
5
|
+
/** The monitoring stack writes the table's name here (infra/monitoring/papyrus-monitoring.yaml). */
|
|
6
|
+
export const MONITORING_TABLE_PARAMETER = "/papyrus/monitoring/table-name";
|
|
7
|
+
|
|
8
|
+
/** Reads only: a single item or a key-range query. No Scan, no writes, no other table. */
|
|
9
|
+
export const MONITORING_VIEW_TABLE_ACTIONS = ["dynamodb:GetItem", "dynamodb:Query"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The monitoring table is auto-named by CloudFormation (`papyrus-monitoring-MonitoringTable-<id>`), so
|
|
13
|
+
* the grant is the stack's table name prefix, not a wildcard on the account.
|
|
14
|
+
*/
|
|
15
|
+
export function monitoringViewTableArns(region: string, account: string): string[] {
|
|
16
|
+
const table = `arn:aws:dynamodb:${region}:${account}:table/papyrus-monitoring-MonitoringTable-*`;
|
|
17
|
+
return [table, `${table}/index/routing`];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function monitoringViewParameterArn(region: string, account: string): string {
|
|
21
|
+
return `arn:aws:ssm:${region}:${account}:parameter${MONITORING_TABLE_PARAMETER}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Environment and least-privilege IAM for the monitoring-view function. A site outside the monitoring
|
|
26
|
+
* account simply finds no parameter and the page says monitoring is not configured.
|
|
27
|
+
*/
|
|
28
|
+
export function wireMonitoringView(backend: any): void {
|
|
29
|
+
const lambda = backend.monitoringView.resources.lambda as LambdaFunction;
|
|
30
|
+
const stack = Stack.of(lambda);
|
|
31
|
+
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl as string);
|
|
32
|
+
lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
|
|
33
|
+
lambda.addToRolePolicy(
|
|
34
|
+
new PolicyStatement({
|
|
35
|
+
effect: Effect.ALLOW,
|
|
36
|
+
actions: ["ssm:GetParameter"],
|
|
37
|
+
resources: [monitoringViewParameterArn(stack.region, stack.account)],
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
lambda.addToRolePolicy(
|
|
41
|
+
new PolicyStatement({
|
|
42
|
+
effect: Effect.ALLOW,
|
|
43
|
+
actions: MONITORING_VIEW_TABLE_ACTIONS,
|
|
44
|
+
resources: monitoringViewTableArns(stack.region, stack.account),
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -45,6 +45,49 @@ function lookUpPatternRedirect(uri) {
|
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Content negotiation for agents: when the site publishes Markdown twins (standard front end, agents.markdown), a request
|
|
49
|
+
// whose Accept header prefers text/markdown over text/html is rewritten to the .md object before the cache lookup. The
|
|
50
|
+
// flag is spliced in by redirect-rules.ts at deploy time.
|
|
51
|
+
const negotiateMarkdown = /*PAPYRUS_NEGOTIATE_MARKDOWN*/false/*END_PAPYRUS_NEGOTIATE_MARKDOWN*/;
|
|
52
|
+
|
|
53
|
+
function qualityOf(accept, mediaType) {
|
|
54
|
+
let quality = 0;
|
|
55
|
+
const parts = accept.toLowerCase().split(",");
|
|
56
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
57
|
+
const fields = parts[index].split(";");
|
|
58
|
+
if (fields[0].trim() !== mediaType) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
quality = 1;
|
|
62
|
+
for (let field = 1; field < fields.length; field += 1) {
|
|
63
|
+
const pair = fields[field].trim();
|
|
64
|
+
if (pair.indexOf("q=") === 0) {
|
|
65
|
+
quality = Number(pair.substring(2));
|
|
66
|
+
if (isNaN(quality)) {
|
|
67
|
+
quality = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return quality;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function prefersMarkdown(request) {
|
|
76
|
+
const header = request.headers && request.headers.accept;
|
|
77
|
+
if (!header || !header.value) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const markdown = qualityOf(header.value, "text/markdown");
|
|
81
|
+
return markdown > 0 && markdown > qualityOf(header.value, "text/html");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function markdownTwin(uri) {
|
|
85
|
+
if (uri === "/404.html" || uri.length < 6 || uri.substring(uri.length - 5) !== ".html") {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return uri.substring(0, uri.length - 5) + ".md";
|
|
89
|
+
}
|
|
90
|
+
|
|
48
91
|
async function handler(event) {
|
|
49
92
|
const request = event.request;
|
|
50
93
|
const redirectTarget = await lookUpRedirectTarget(request.uri);
|
|
@@ -64,5 +107,11 @@ async function handler(event) {
|
|
|
64
107
|
};
|
|
65
108
|
}
|
|
66
109
|
request.uri = rewriteToIndexDocument(request.uri);
|
|
110
|
+
if (negotiateMarkdown && prefersMarkdown(request)) {
|
|
111
|
+
const twin = markdownTwin(request.uri);
|
|
112
|
+
if (twin !== null) {
|
|
113
|
+
request.uri = twin;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
67
116
|
return request;
|
|
68
117
|
}
|
|
@@ -18,23 +18,39 @@ export function edgeRulesFromSettings(settings) {
|
|
|
18
18
|
}
|
|
19
19
|
return rules;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
/** A standard-front-end site publishes Markdown twins unless it turns `agents.markdown` off. */
|
|
22
|
+
export function negotiatesMarkdown(settings) {
|
|
23
|
+
return settings.site !== undefined && settings.agents?.markdown !== false;
|
|
24
|
+
}
|
|
25
|
+
export function readEdgeConfig(staticDir, baseDirectory = process.cwd()) {
|
|
22
26
|
if (staticDir === undefined)
|
|
23
|
-
return [];
|
|
27
|
+
return { rules: [], negotiateMarkdown: false };
|
|
24
28
|
const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
|
|
25
29
|
if (!existsSync(settingsPath))
|
|
26
|
-
return [];
|
|
27
|
-
|
|
30
|
+
return { rules: [], negotiateMarkdown: false };
|
|
31
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
32
|
+
return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings) };
|
|
28
33
|
}
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
export function readEdgeRules(staticDir, baseDirectory = process.cwd()) {
|
|
35
|
+
return readEdgeConfig(staticDir, baseDirectory).rules;
|
|
36
|
+
}
|
|
37
|
+
const NEGOTIATE_START = "/*PAPYRUS_NEGOTIATE_MARKDOWN*/";
|
|
38
|
+
const NEGOTIATE_END = "/*END_PAPYRUS_NEGOTIATE_MARKDOWN*/";
|
|
39
|
+
/** The CloudFront Function source with the site's pattern rules and Markdown negotiation flag spliced in. */
|
|
40
|
+
export function compileRedirectFunctionSource(source, rules, options = {}) {
|
|
31
41
|
const start = source.indexOf(START_MARKER);
|
|
32
42
|
const end = source.indexOf(END_MARKER);
|
|
33
43
|
if (start === -1 || end === -1 || end < start) {
|
|
34
44
|
throw new Error("redirect function source has no pattern-rule markers");
|
|
35
45
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
let compiled = rules.length === 0 ? source : source.slice(0, start + START_MARKER.length) + JSON.stringify(rules) + source.slice(end);
|
|
47
|
+
if (options.negotiateMarkdown === true) {
|
|
48
|
+
const from = compiled.indexOf(NEGOTIATE_START);
|
|
49
|
+
const to = compiled.indexOf(NEGOTIATE_END);
|
|
50
|
+
if (from === -1 || to === -1 || to < from) {
|
|
51
|
+
throw new Error("redirect function source has no markdown negotiation markers");
|
|
52
|
+
}
|
|
53
|
+
compiled = compiled.slice(0, from + NEGOTIATE_START.length) + "true" + compiled.slice(to);
|
|
54
|
+
}
|
|
55
|
+
return compiled;
|
|
40
56
|
}
|
|
@@ -8,7 +8,7 @@ const START_MARKER = "/*PAPYRUS_PATTERN_RULES*/";
|
|
|
8
8
|
const END_MARKER = "/*END_PAPYRUS_PATTERN_RULES*/";
|
|
9
9
|
|
|
10
10
|
type ProjectionRedirects = { patterns?: { pattern: string; to: string; status?: number; except?: string[] }[] };
|
|
11
|
-
type ProjectionSettings = { urls?: { style?: string }; redirects?: ProjectionRedirects };
|
|
11
|
+
type ProjectionSettings = { site?: unknown; urls?: { style?: string }; redirects?: ProjectionRedirects; agents?: { markdown?: boolean } };
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* The pattern rules a site declares in projection.json: its `redirects.patterns`, plus, for
|
|
@@ -27,21 +27,47 @@ export function edgeRulesFromSettings(settings: ProjectionSettings): EdgeRedirec
|
|
|
27
27
|
return rules;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
/** A standard-front-end site publishes Markdown twins unless it turns `agents.markdown` off. */
|
|
31
|
+
export function negotiatesMarkdown(settings: ProjectionSettings): boolean {
|
|
32
|
+
return settings.site !== undefined && settings.agents?.markdown !== false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type EdgeConfig = { rules: EdgeRedirectRule[]; negotiateMarkdown: boolean };
|
|
36
|
+
|
|
37
|
+
export function readEdgeConfig(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeConfig {
|
|
38
|
+
if (staticDir === undefined) return { rules: [], negotiateMarkdown: false };
|
|
32
39
|
const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
|
|
33
|
-
if (!existsSync(settingsPath)) return [];
|
|
34
|
-
|
|
40
|
+
if (!existsSync(settingsPath)) return { rules: [], negotiateMarkdown: false };
|
|
41
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as ProjectionSettings;
|
|
42
|
+
return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings) };
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
export function readEdgeRules(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeRedirectRule[] {
|
|
46
|
+
return readEdgeConfig(staticDir, baseDirectory).rules;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const NEGOTIATE_START = "/*PAPYRUS_NEGOTIATE_MARKDOWN*/";
|
|
50
|
+
const NEGOTIATE_END = "/*END_PAPYRUS_NEGOTIATE_MARKDOWN*/";
|
|
51
|
+
|
|
52
|
+
/** The CloudFront Function source with the site's pattern rules and Markdown negotiation flag spliced in. */
|
|
53
|
+
export function compileRedirectFunctionSource(
|
|
54
|
+
source: string,
|
|
55
|
+
rules: EdgeRedirectRule[],
|
|
56
|
+
options: { negotiateMarkdown?: boolean } = {},
|
|
57
|
+
): string {
|
|
39
58
|
const start = source.indexOf(START_MARKER);
|
|
40
59
|
const end = source.indexOf(END_MARKER);
|
|
41
60
|
if (start === -1 || end === -1 || end < start) {
|
|
42
61
|
throw new Error("redirect function source has no pattern-rule markers");
|
|
43
62
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
let compiled = rules.length === 0 ? source : source.slice(0, start + START_MARKER.length) + JSON.stringify(rules) + source.slice(end);
|
|
64
|
+
if (options.negotiateMarkdown === true) {
|
|
65
|
+
const from = compiled.indexOf(NEGOTIATE_START);
|
|
66
|
+
const to = compiled.indexOf(NEGOTIATE_END);
|
|
67
|
+
if (from === -1 || to === -1 || to < from) {
|
|
68
|
+
throw new Error("redirect function source has no markdown negotiation markers");
|
|
69
|
+
}
|
|
70
|
+
compiled = compiled.slice(0, from + NEGOTIATE_START.length) + "true" + compiled.slice(to);
|
|
71
|
+
}
|
|
72
|
+
return compiled;
|
|
47
73
|
}
|
|
@@ -13,7 +13,7 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
import { assertValidPublicationConfig } from "../site-backend-config.js";
|
|
14
14
|
import { addRenditionsFunction } from "./renditions-function.js";
|
|
15
15
|
import { RENDITIONS_FUNCTION_NAME_ENV } from "./renditions-settings.js";
|
|
16
|
-
import { compileRedirectFunctionSource,
|
|
16
|
+
import { compileRedirectFunctionSource, readEdgeConfig } from "./redirect-rules.js";
|
|
17
17
|
import { addInvalidationPathAlarm } from "./invalidation-alarm.js";
|
|
18
18
|
import { distributionIdFromArn, s3ProjectionEnvironment, s3ProjectionPolicyStatements } from "./s3-projection-settings.js";
|
|
19
19
|
const redirectFunctionSourcePath = resolve(dirname(fileURLToPath(import.meta.url)), "redirect-and-index.js");
|
|
@@ -72,7 +72,9 @@ export function addS3ProjectionPublication(scope, props, media) {
|
|
|
72
72
|
const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
|
|
73
73
|
const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
|
|
74
74
|
runtime: cloudfront.FunctionRuntime.JS_2_0,
|
|
75
|
-
code: cloudfront.FunctionCode.fromInline(compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"),
|
|
75
|
+
code: cloudfront.FunctionCode.fromInline(compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeConfig(props.staticDir).rules, {
|
|
76
|
+
negotiateMarkdown: readEdgeConfig(props.staticDir).negotiateMarkdown,
|
|
77
|
+
})),
|
|
76
78
|
keyValueStore,
|
|
77
79
|
});
|
|
78
80
|
const deadLetterQueue = new sqs.Queue(scope, "ProjectionDeadLetterQueue", {
|
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import { assertValidPublicationConfig, type PapyrusPublicationConfig } from "../site-backend-config";
|
|
17
17
|
import { addRenditionsFunction, type RenditionsFunction } from "./renditions-function";
|
|
18
18
|
import { RENDITIONS_FUNCTION_NAME_ENV, type RenditionsPlan } from "./renditions-settings";
|
|
19
|
-
import { compileRedirectFunctionSource,
|
|
19
|
+
import { compileRedirectFunctionSource, readEdgeConfig } from "./redirect-rules";
|
|
20
20
|
import { addInvalidationPathAlarm } from "./invalidation-alarm";
|
|
21
21
|
import { distributionIdFromArn, s3ProjectionEnvironment, s3ProjectionPolicyStatements } from "./s3-projection-settings";
|
|
22
22
|
|
|
@@ -115,7 +115,9 @@ export function addS3ProjectionPublication(
|
|
|
115
115
|
const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
|
|
116
116
|
runtime: cloudfront.FunctionRuntime.JS_2_0,
|
|
117
117
|
code: cloudfront.FunctionCode.fromInline(
|
|
118
|
-
compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"),
|
|
118
|
+
compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeConfig(props.staticDir).rules, {
|
|
119
|
+
negotiateMarkdown: readEdgeConfig(props.staticDir).negotiateMarkdown,
|
|
120
|
+
}),
|
|
119
121
|
),
|
|
120
122
|
keyValueStore,
|
|
121
123
|
});
|
package/amplify/site-backend.js
CHANGED
|
@@ -21,6 +21,8 @@ import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth, siteAuthRe
|
|
|
21
21
|
import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client.js";
|
|
22
22
|
import { defineSiteData } from "./data/resource.js";
|
|
23
23
|
import { editorialChecks } from "./functions/editorial-checks/resource.js";
|
|
24
|
+
import { monitoringView } from "./functions/monitoring-view/resource.js";
|
|
25
|
+
import { wireMonitoringView } from "./functions/monitoring-view/wire.js";
|
|
24
26
|
import { wireEditorialChecks } from "./functions/editorial-checks/wire.js";
|
|
25
27
|
import { resolvePlugins } from "./plugins/registry.js";
|
|
26
28
|
import { wirePlugins } from "./plugins/wire.js";
|
|
@@ -95,6 +97,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
95
97
|
data: defineSiteData(pluginDefinitions),
|
|
96
98
|
knowledgeQuery,
|
|
97
99
|
manageUserRole,
|
|
100
|
+
monitoringView,
|
|
98
101
|
modelAttachmentUpload,
|
|
99
102
|
newsroomSummary,
|
|
100
103
|
procedureAction,
|
|
@@ -598,6 +601,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
598
601
|
},
|
|
599
602
|
});
|
|
600
603
|
wireEditorialChecks(backend);
|
|
604
|
+
wireMonitoringView(backend);
|
|
601
605
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
602
606
|
return backend;
|
|
603
607
|
}
|
package/amplify/site-backend.ts
CHANGED
|
@@ -28,6 +28,8 @@ import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth, siteAuthRe
|
|
|
28
28
|
import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client";
|
|
29
29
|
import { defineSiteData } from "./data/resource";
|
|
30
30
|
import { editorialChecks } from "./functions/editorial-checks/resource";
|
|
31
|
+
import { monitoringView } from "./functions/monitoring-view/resource";
|
|
32
|
+
import { wireMonitoringView } from "./functions/monitoring-view/wire";
|
|
31
33
|
import { wireEditorialChecks } from "./functions/editorial-checks/wire";
|
|
32
34
|
import { resolvePlugins } from "./plugins/registry";
|
|
33
35
|
import { wirePlugins } from "./plugins/wire";
|
|
@@ -116,6 +118,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
116
118
|
data: defineSiteData(pluginDefinitions),
|
|
117
119
|
knowledgeQuery,
|
|
118
120
|
manageUserRole,
|
|
121
|
+
monitoringView,
|
|
119
122
|
modelAttachmentUpload,
|
|
120
123
|
newsroomSummary,
|
|
121
124
|
procedureAction,
|
|
@@ -741,6 +744,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
741
744
|
},
|
|
742
745
|
});
|
|
743
746
|
wireEditorialChecks(backend);
|
|
747
|
+
wireMonitoringView(backend);
|
|
744
748
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
745
749
|
|
|
746
750
|
return backend;
|