@anthusai/papyrus 3.3.0 → 5.0.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/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +1 -1
- 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 +92 -0
- package/amplify/{plugins → functions}/editorial-checks/schema.ts +4 -6
- package/amplify/functions/editorial-checks/wire.js +49 -0
- package/amplify/functions/editorial-checks/wire.ts +57 -0
- package/amplify/plugins/registry.js +0 -2
- package/amplify/plugins/registry.ts +0 -2
- package/amplify/publication/redirect-and-index.js +31 -0
- package/amplify/publication/redirect-rules.js +40 -0
- package/amplify/publication/redirect-rules.ts +47 -0
- package/amplify/publication/s3-projection.js +3 -1
- package/amplify/publication/s3-projection.ts +5 -1
- package/amplify/site-backend.js +4 -0
- package/amplify/site-backend.ts +4 -0
- package/components/editorial-checks-admin-panel.tsx +19 -9
- package/components/newsroom-article-editor.tsx +2 -4
- package/components/topic-steering-workspace.tsx +10 -17
- package/components/use-editorial-checks.ts +12 -10
- package/lib/category-repository.ts +10 -8
- package/lib/editorial-checks-backend.ts +1 -1
- package/lib/editorial-checks-client.ts +1 -1
- package/lib/editorial-checks-demo.ts +2 -2
- package/lib/editorial-checks-settings.ts +3 -0
- package/lib/editorial-checks.ts +2 -9
- package/lib/editorial-preview.ts +5 -5
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +6 -3
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
- package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
- package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
- package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
- package/amplify/plugins/editorial-checks/schema.js +0 -94
package/amplify/data/schema.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema.js";
|
|
4
5
|
import { contentActions } from "../functions/content-actions/resource.js";
|
|
5
6
|
import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
|
|
6
7
|
import { manageUserRole } from "../functions/manage-user-role/resource.js";
|
|
@@ -1993,6 +1994,7 @@ const baseDefinitions = {
|
|
|
1993
1994
|
allow.guest().to(["read"]),
|
|
1994
1995
|
allow.groups(contentWriteGroups),
|
|
1995
1996
|
]),
|
|
1997
|
+
...editorialChecksSchemaDefinitions,
|
|
1996
1998
|
};
|
|
1997
1999
|
export const schema = a.schema(baseDefinitions).authorization((allow) => [
|
|
1998
2000
|
allow.resource(assignmentAction).to(["query", "mutate"]),
|
package/amplify/data/schema.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema";
|
|
4
5
|
import { contentActions } from "../functions/content-actions/resource";
|
|
5
6
|
import { knowledgeQuery } from "../functions/knowledge-query/resource";
|
|
6
7
|
import { manageUserRole } from "../functions/manage-user-role/resource";
|
|
@@ -2112,6 +2113,7 @@ const baseDefinitions = {
|
|
|
2112
2113
|
allow.guest().to(["read"]),
|
|
2113
2114
|
allow.groups(contentWriteGroups),
|
|
2114
2115
|
]),
|
|
2116
|
+
...editorialChecksSchemaDefinitions,
|
|
2115
2117
|
};
|
|
2116
2118
|
|
|
2117
2119
|
export const schema = a.schema(baseDefinitions).authorization((allow) => [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
humanus[jev]>=0.33,<0.34
|
package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js
RENAMED
|
@@ -4,12 +4,12 @@ import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { pythonLambdaCode } from "
|
|
7
|
+
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
8
8
|
const functionDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
/** AppSync gives a resolver 30 seconds;
|
|
9
|
+
/** AppSync gives a resolver 30 seconds; Humanus's classifier deadline (settings, default 20 s) keeps a check inside this. */
|
|
10
10
|
export const EDITORIAL_CHECKS_TIMEOUT_SECONDS = 28;
|
|
11
|
-
/**
|
|
12
|
-
export const editorialChecksRequirements = ["
|
|
11
|
+
/** Humanus with the Jev classifier lane; papyrus_content.editorial_checks does the work. */
|
|
12
|
+
export const editorialChecksRequirements = ["humanus[jev]>=0.33,<0.34"];
|
|
13
13
|
function installRequirements(outputDir) {
|
|
14
14
|
execFileSync("python3", [
|
|
15
15
|
"-m", "pip", "install", "-r", path.join(functionDirectory, "requirements.txt"), "-t", outputDir, "--no-cache-dir",
|
|
@@ -24,7 +24,7 @@ export const editorialChecks = defineFunction((scope) => new Function(scope, "pa
|
|
|
24
24
|
timeout: Duration.seconds(EDITORIAL_CHECKS_TIMEOUT_SECONDS),
|
|
25
25
|
memorySize: 1024,
|
|
26
26
|
code: pythonLambdaCode({
|
|
27
|
-
functionDir: "
|
|
27
|
+
functionDir: "editorial-checks",
|
|
28
28
|
modules: ["papyrus_content"],
|
|
29
29
|
requirements: editorialChecksRequirements,
|
|
30
30
|
noDeps: true,
|
|
@@ -35,9 +35,9 @@ export const editorialChecks = defineFunction((scope) => new Function(scope, "pa
|
|
|
35
35
|
[
|
|
36
36
|
"set -euo pipefail",
|
|
37
37
|
"mkdir -p /asset-output/papyrus_content",
|
|
38
|
-
"cp amplify/
|
|
38
|
+
"cp amplify/functions/editorial-checks/handler.py /asset-output/handler.py",
|
|
39
39
|
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
40
|
-
"python3 -m pip install -r amplify/
|
|
40
|
+
"python3 -m pip install -r amplify/functions/editorial-checks/requirements.txt -t /asset-output --no-cache-dir",
|
|
41
41
|
].join(" && "),
|
|
42
42
|
]),
|
|
43
43
|
}), { resourceGroupName: "data" });
|
package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts
RENAMED
|
@@ -5,15 +5,15 @@ import type { Construct } from "constructs";
|
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { pythonLambdaCode } from "
|
|
8
|
+
import { pythonLambdaCode } from "../shared/python-bundle";
|
|
9
9
|
|
|
10
10
|
const functionDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
|
|
12
|
-
/** AppSync gives a resolver 30 seconds;
|
|
12
|
+
/** AppSync gives a resolver 30 seconds; Humanus's classifier deadline (settings, default 20 s) keeps a check inside this. */
|
|
13
13
|
export const EDITORIAL_CHECKS_TIMEOUT_SECONDS = 28;
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
export const editorialChecksRequirements = ["
|
|
15
|
+
/** Humanus with the Jev classifier lane; papyrus_content.editorial_checks does the work. */
|
|
16
|
+
export const editorialChecksRequirements = ["humanus[jev]>=0.33,<0.34"];
|
|
17
17
|
|
|
18
18
|
function installRequirements(outputDir: string): void {
|
|
19
19
|
execFileSync(
|
|
@@ -37,7 +37,7 @@ export const editorialChecks = defineFunction(
|
|
|
37
37
|
memorySize: 1024,
|
|
38
38
|
code: pythonLambdaCode(
|
|
39
39
|
{
|
|
40
|
-
functionDir: "
|
|
40
|
+
functionDir: "editorial-checks",
|
|
41
41
|
modules: ["papyrus_content"],
|
|
42
42
|
requirements: editorialChecksRequirements,
|
|
43
43
|
noDeps: true,
|
|
@@ -49,9 +49,9 @@ export const editorialChecks = defineFunction(
|
|
|
49
49
|
[
|
|
50
50
|
"set -euo pipefail",
|
|
51
51
|
"mkdir -p /asset-output/papyrus_content",
|
|
52
|
-
"cp amplify/
|
|
52
|
+
"cp amplify/functions/editorial-checks/handler.py /asset-output/handler.py",
|
|
53
53
|
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
54
|
-
"python3 -m pip install -r amplify/
|
|
54
|
+
"python3 -m pip install -r amplify/functions/editorial-checks/requirements.txt -t /asset-output --no-cache-dir",
|
|
55
55
|
].join(" && "),
|
|
56
56
|
],
|
|
57
57
|
),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
import { editorialChecks } from "./resource.js";
|
|
3
|
+
const editors = ["editor", "admin"];
|
|
4
|
+
/**
|
|
5
|
+
* Core to every site's data API. Policies, settings and editors' decisions live in Papyrus; Humanus checks
|
|
6
|
+
* content against them. Editors read policies and record decisions; admins
|
|
7
|
+
* manage policies and settings. Only the function (IAM) calls Humanus.
|
|
8
|
+
*/
|
|
9
|
+
export const editorialChecksSchemaDefinitions = {
|
|
10
|
+
EditorialPolicy: a
|
|
11
|
+
.model({
|
|
12
|
+
id: a.id().required(),
|
|
13
|
+
policyKey: a.string().required(),
|
|
14
|
+
scope: a.string().required(),
|
|
15
|
+
version: a.integer().required(),
|
|
16
|
+
title: a.string().required(),
|
|
17
|
+
guidance: a.string().required(),
|
|
18
|
+
severity: a.string().required(),
|
|
19
|
+
enabled: a.boolean().required(),
|
|
20
|
+
detectors: a.json().required(),
|
|
21
|
+
examples: a.json(),
|
|
22
|
+
rewriteGuidance: a.string(),
|
|
23
|
+
source: a.string().required(),
|
|
24
|
+
})
|
|
25
|
+
.disableOperations(["subscriptions"])
|
|
26
|
+
.authorization((allow) => [allow.group("editor").to(["read"]), allow.group("admin")]),
|
|
27
|
+
EditorialCheckSettings: a
|
|
28
|
+
.model({
|
|
29
|
+
id: a.id().required(),
|
|
30
|
+
settings: a.json().required(),
|
|
31
|
+
})
|
|
32
|
+
.disableOperations(["subscriptions"])
|
|
33
|
+
.authorization((allow) => [allow.group("editor").to(["read"]), allow.group("admin")]),
|
|
34
|
+
EditorialFeedback: a
|
|
35
|
+
.model({
|
|
36
|
+
id: a.id().required(),
|
|
37
|
+
itemId: a.string().required(),
|
|
38
|
+
findingId: a.string().required(),
|
|
39
|
+
policyId: a.string().required(),
|
|
40
|
+
detectorId: a.string().required(),
|
|
41
|
+
lane: a.string().required(),
|
|
42
|
+
probability: a.float(),
|
|
43
|
+
decision: a.string().required(),
|
|
44
|
+
reason: a.string(),
|
|
45
|
+
text: a.string(),
|
|
46
|
+
})
|
|
47
|
+
.disableOperations(["subscriptions"])
|
|
48
|
+
.secondaryIndexes((index) => [index("itemId").queryField("listEditorialFeedbackByItem")])
|
|
49
|
+
.authorization((allow) => [allow.group("editor").to(["create", "read"]), allow.group("admin")]),
|
|
50
|
+
checkEditorial: a
|
|
51
|
+
.query()
|
|
52
|
+
.arguments({ body: a.string().required(), format: a.string(), section: a.string() })
|
|
53
|
+
.returns(a.json())
|
|
54
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
55
|
+
.handler(a.handler.function(editorialChecks)),
|
|
56
|
+
suggestEditorialRewrite: a
|
|
57
|
+
.query()
|
|
58
|
+
.arguments({
|
|
59
|
+
body: a.string().required(),
|
|
60
|
+
format: a.string(),
|
|
61
|
+
section: a.string(),
|
|
62
|
+
finding: a.json(),
|
|
63
|
+
selection: a.json(),
|
|
64
|
+
note: a.string(),
|
|
65
|
+
})
|
|
66
|
+
.returns(a.json())
|
|
67
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
68
|
+
.handler(a.handler.function(editorialChecks)),
|
|
69
|
+
draftEditorialQuestions: a
|
|
70
|
+
.query()
|
|
71
|
+
.arguments({ title: a.string().required(), guidance: a.string().required(), examples: a.json() })
|
|
72
|
+
.returns(a.json())
|
|
73
|
+
.authorization((allow) => [allow.group("admin")])
|
|
74
|
+
.handler(a.handler.function(editorialChecks)),
|
|
75
|
+
tryEditorialPolicy: a
|
|
76
|
+
.query()
|
|
77
|
+
.arguments({ policy: a.json().required(), text: a.string().required(), format: a.string() })
|
|
78
|
+
.returns(a.json())
|
|
79
|
+
.authorization((allow) => [allow.group("admin")])
|
|
80
|
+
.handler(a.handler.function(editorialChecks)),
|
|
81
|
+
importEditorialStarterPack: a
|
|
82
|
+
.mutation()
|
|
83
|
+
.arguments({ name: a.string().required() })
|
|
84
|
+
.returns(a.json())
|
|
85
|
+
.authorization((allow) => [allow.group("admin")])
|
|
86
|
+
.handler(a.handler.function(editorialChecks)),
|
|
87
|
+
recalibrateEditorialPolicies: a
|
|
88
|
+
.mutation()
|
|
89
|
+
.returns(a.json())
|
|
90
|
+
.authorization((allow) => [allow.group("admin")])
|
|
91
|
+
.handler(a.handler.function(editorialChecks)),
|
|
92
|
+
};
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { a } from "@aws-amplify/backend";
|
|
2
|
-
import { editorialChecks } from "./
|
|
2
|
+
import { editorialChecks } from "./resource";
|
|
3
3
|
|
|
4
4
|
const editors = ["editor", "admin"];
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Policies, settings and editors' decisions live in Papyrus;
|
|
7
|
+
* Core to every site's data API. Policies, settings and editors' decisions live in Papyrus; Humanus checks
|
|
8
8
|
* content against them. Editors read policies and record decisions; admins
|
|
9
|
-
* manage policies and settings. Only the function (IAM) calls
|
|
9
|
+
* manage policies and settings. Only the function (IAM) calls Humanus.
|
|
10
10
|
*/
|
|
11
|
-
export
|
|
12
|
-
return {
|
|
11
|
+
export const editorialChecksSchemaDefinitions = {
|
|
13
12
|
EditorialPolicy: a
|
|
14
13
|
.model({
|
|
15
14
|
id: a.id().required(),
|
|
@@ -93,4 +92,3 @@ export function editorialChecksSchemaDefinitions() {
|
|
|
93
92
|
.authorization((allow) => [allow.group("admin")])
|
|
94
93
|
.handler(a.handler.function(editorialChecks)),
|
|
95
94
|
};
|
|
96
|
-
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { secret } from "@aws-amplify/backend";
|
|
2
|
+
import { Stack } from "aws-cdk-lib";
|
|
3
|
+
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
4
|
+
/** The model operations the function performs with its own role. The two context reads are its least-privilege access to private policy. */
|
|
5
|
+
export const EDITORIAL_CHECKS_FUNCTION_FIELDS = [
|
|
6
|
+
"Query/fields/listKnowledgeCorporaByRoleAndName",
|
|
7
|
+
"Query/fields/getEditorialContext",
|
|
8
|
+
"Query/fields/listEditorialPolicies",
|
|
9
|
+
"Query/fields/getEditorialCheckSettings",
|
|
10
|
+
"Query/fields/listEditorialFeedbacks",
|
|
11
|
+
"Mutation/fields/createEditorialPolicy",
|
|
12
|
+
"Mutation/fields/updateEditorialPolicy",
|
|
13
|
+
];
|
|
14
|
+
export const EDITORIAL_CHECKS_SECRETS = ["TYPESAFE_API_KEY", "OPENAI_API_KEY"];
|
|
15
|
+
/**
|
|
16
|
+
* SSM parameters holding the provider keys. A Python function reads its secret()
|
|
17
|
+
* values itself (Amplify only resolves them for Node functions), so it needs read
|
|
18
|
+
* access to the app's branch and shared secret paths, and to the sandbox paths.
|
|
19
|
+
*/
|
|
20
|
+
export function editorialChecksSecretParameters(region, account, appId) {
|
|
21
|
+
const arn = (path) => `arn:aws:ssm:${region}:${account}:parameter/amplify/${path}`;
|
|
22
|
+
return EDITORIAL_CHECKS_SECRETS.flatMap((name) => [
|
|
23
|
+
arn(`papyrus/*/${name}`),
|
|
24
|
+
arn(`shared/papyrus/${name}`),
|
|
25
|
+
...(appId ? [arn(`${appId}/*/${name}`), arn(`shared/${appId}/${name}`)] : []),
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Environment, secrets and least-privilege IAM for the editorial-checks function. Every site gets
|
|
30
|
+
* these; whether checks run is a stored setting (EditorialCheckSettings.enabled), not a deploy flag.
|
|
31
|
+
*/
|
|
32
|
+
export function wireEditorialChecks(backend) {
|
|
33
|
+
const lambda = backend.editorialChecks.resources.lambda;
|
|
34
|
+
const stack = Stack.of(lambda);
|
|
35
|
+
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
|
|
36
|
+
for (const name of EDITORIAL_CHECKS_SECRETS)
|
|
37
|
+
backend.editorialChecks.addEnvironment(name, secret(name));
|
|
38
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
39
|
+
effect: Effect.ALLOW,
|
|
40
|
+
actions: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
41
|
+
resources: editorialChecksSecretParameters(stack.region, stack.account, (process.env.AWS_APP_ID ?? "").trim()),
|
|
42
|
+
}));
|
|
43
|
+
const apiId = backend.data.resources.graphqlApi.apiId;
|
|
44
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
45
|
+
effect: Effect.ALLOW,
|
|
46
|
+
actions: ["appsync:GraphQL"],
|
|
47
|
+
resources: EDITORIAL_CHECKS_FUNCTION_FIELDS.map((field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${apiId}/types/${field}`),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { secret } from "@aws-amplify/backend";
|
|
2
|
+
import { Stack } from "aws-cdk-lib";
|
|
3
|
+
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
4
|
+
import type { Function as LambdaFunction } from "aws-cdk-lib/aws-lambda";
|
|
5
|
+
|
|
6
|
+
/** The model operations the function performs with its own role. The two context reads are its least-privilege access to private policy. */
|
|
7
|
+
export const EDITORIAL_CHECKS_FUNCTION_FIELDS = [
|
|
8
|
+
"Query/fields/listKnowledgeCorporaByRoleAndName",
|
|
9
|
+
"Query/fields/getEditorialContext",
|
|
10
|
+
"Query/fields/listEditorialPolicies",
|
|
11
|
+
"Query/fields/getEditorialCheckSettings",
|
|
12
|
+
"Query/fields/listEditorialFeedbacks",
|
|
13
|
+
"Mutation/fields/createEditorialPolicy",
|
|
14
|
+
"Mutation/fields/updateEditorialPolicy",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const EDITORIAL_CHECKS_SECRETS = ["TYPESAFE_API_KEY", "OPENAI_API_KEY"];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* SSM parameters holding the provider keys. A Python function reads its secret()
|
|
21
|
+
* values itself (Amplify only resolves them for Node functions), so it needs read
|
|
22
|
+
* access to the app's branch and shared secret paths, and to the sandbox paths.
|
|
23
|
+
*/
|
|
24
|
+
export function editorialChecksSecretParameters(region: string, account: string, appId: string): string[] {
|
|
25
|
+
const arn = (path: string) => `arn:aws:ssm:${region}:${account}:parameter/amplify/${path}`;
|
|
26
|
+
return EDITORIAL_CHECKS_SECRETS.flatMap((name) => [
|
|
27
|
+
arn(`papyrus/*/${name}`),
|
|
28
|
+
arn(`shared/papyrus/${name}`),
|
|
29
|
+
...(appId ? [arn(`${appId}/*/${name}`), arn(`shared/${appId}/${name}`)] : []),
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Environment, secrets and least-privilege IAM for the editorial-checks function. Every site gets
|
|
35
|
+
* these; whether checks run is a stored setting (EditorialCheckSettings.enabled), not a deploy flag.
|
|
36
|
+
*/
|
|
37
|
+
export function wireEditorialChecks(backend: any): void {
|
|
38
|
+
const lambda = backend.editorialChecks.resources.lambda as LambdaFunction;
|
|
39
|
+
const stack = Stack.of(lambda);
|
|
40
|
+
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl as string);
|
|
41
|
+
for (const name of EDITORIAL_CHECKS_SECRETS) backend.editorialChecks.addEnvironment(name, secret(name));
|
|
42
|
+
lambda.addToRolePolicy(
|
|
43
|
+
new PolicyStatement({
|
|
44
|
+
effect: Effect.ALLOW,
|
|
45
|
+
actions: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
46
|
+
resources: editorialChecksSecretParameters(stack.region, stack.account, (process.env.AWS_APP_ID ?? "").trim()),
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
const apiId = backend.data.resources.graphqlApi.apiId as string;
|
|
50
|
+
lambda.addToRolePolicy(
|
|
51
|
+
new PolicyStatement({
|
|
52
|
+
effect: Effect.ALLOW,
|
|
53
|
+
actions: ["appsync:GraphQL"],
|
|
54
|
+
resources: EDITORIAL_CHECKS_FUNCTION_FIELDS.map((field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${apiId}/types/${field}`),
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { editorialChecksPlugin } from "./editorial-checks/papyrus.plugin.js";
|
|
2
1
|
import { signupPlugin } from "./signup/papyrus.plugin.js";
|
|
3
2
|
/** Plugins are packaged with Papyrus and pinned by its version: there is no registry to fetch from. */
|
|
4
3
|
export const KNOWN_PLUGINS = {
|
|
5
|
-
"editorial-checks": editorialChecksPlugin,
|
|
6
4
|
signup: signupPlugin,
|
|
7
5
|
};
|
|
8
6
|
export function resolvePlugins(requests) {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { PapyrusPlugin, PluginRequest } from "./types";
|
|
2
|
-
import { editorialChecksPlugin } from "./editorial-checks/papyrus.plugin";
|
|
3
2
|
import { signupPlugin } from "./signup/papyrus.plugin";
|
|
4
3
|
|
|
5
4
|
/** Plugins are packaged with Papyrus and pinned by its version: there is no registry to fetch from. */
|
|
6
5
|
export const KNOWN_PLUGINS: Record<string, PapyrusPlugin<any, any>> = {
|
|
7
|
-
"editorial-checks": editorialChecksPlugin,
|
|
8
6
|
signup: signupPlugin,
|
|
9
7
|
};
|
|
10
8
|
|
|
@@ -22,6 +22,29 @@ function rewriteToIndexDocument(uri) {
|
|
|
22
22
|
return uri;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// Pattern redirects: data from the site's projection.json, spliced between the markers at deploy
|
|
26
|
+
// time by redirect-rules.ts. Each rule is {p: regex, t: target with $1..$9, s: status, x: excluded paths}.
|
|
27
|
+
const patternRules = /*PAPYRUS_PATTERN_RULES*/[]/*END_PAPYRUS_PATTERN_RULES*/;
|
|
28
|
+
|
|
29
|
+
function lookUpPatternRedirect(uri) {
|
|
30
|
+
for (let index = 0; index < patternRules.length; index += 1) {
|
|
31
|
+
const rule = patternRules[index];
|
|
32
|
+
if (rule.x.indexOf(uri) !== -1) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const match = new RegExp(rule.p).exec(uri);
|
|
36
|
+
if (match !== null) {
|
|
37
|
+
return {
|
|
38
|
+
status: rule.s,
|
|
39
|
+
target: rule.t.replace(/\$([0-9])/g, function (whole, group) {
|
|
40
|
+
return match[Number(group)] || "";
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
async function handler(event) {
|
|
26
49
|
const request = event.request;
|
|
27
50
|
const redirectTarget = await lookUpRedirectTarget(request.uri);
|
|
@@ -32,6 +55,14 @@ async function handler(event) {
|
|
|
32
55
|
headers: { location: { value: redirectTarget } },
|
|
33
56
|
};
|
|
34
57
|
}
|
|
58
|
+
const patternRedirect = lookUpPatternRedirect(request.uri);
|
|
59
|
+
if (patternRedirect !== null) {
|
|
60
|
+
return {
|
|
61
|
+
statusCode: patternRedirect.status,
|
|
62
|
+
statusDescription: "Moved",
|
|
63
|
+
headers: { location: { value: patternRedirect.target } },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
35
66
|
request.uri = rewriteToIndexDocument(request.uri);
|
|
36
67
|
return request;
|
|
37
68
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const START_MARKER = "/*PAPYRUS_PATTERN_RULES*/";
|
|
4
|
+
const END_MARKER = "/*END_PAPYRUS_PATTERN_RULES*/";
|
|
5
|
+
/**
|
|
6
|
+
* The pattern rules a site declares in projection.json: its `redirects.patterns`, plus, for
|
|
7
|
+
* `urls.style: "directory"`, `/x.html` to `/x/`. Mirrors `StandardSettings.edge_rules` in Python.
|
|
8
|
+
*/
|
|
9
|
+
export function edgeRulesFromSettings(settings) {
|
|
10
|
+
const rules = (settings.redirects?.patterns ?? []).map((rule) => ({
|
|
11
|
+
p: rule.pattern,
|
|
12
|
+
t: rule.to,
|
|
13
|
+
s: rule.status ?? 301,
|
|
14
|
+
x: rule.except ?? [],
|
|
15
|
+
}));
|
|
16
|
+
if (settings.urls?.style === "directory") {
|
|
17
|
+
rules.push({ p: "^/(.+)\\.html$", t: "/$1/", s: 301, x: ["/404.html"] });
|
|
18
|
+
}
|
|
19
|
+
return rules;
|
|
20
|
+
}
|
|
21
|
+
export function readEdgeRules(staticDir, baseDirectory = process.cwd()) {
|
|
22
|
+
if (staticDir === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
|
|
25
|
+
if (!existsSync(settingsPath))
|
|
26
|
+
return [];
|
|
27
|
+
return edgeRulesFromSettings(JSON.parse(readFileSync(settingsPath, "utf8")));
|
|
28
|
+
}
|
|
29
|
+
/** The CloudFront Function source with the site's pattern rules spliced in; the source unchanged when there are none. */
|
|
30
|
+
export function compileRedirectFunctionSource(source, rules) {
|
|
31
|
+
const start = source.indexOf(START_MARKER);
|
|
32
|
+
const end = source.indexOf(END_MARKER);
|
|
33
|
+
if (start === -1 || end === -1 || end < start) {
|
|
34
|
+
throw new Error("redirect function source has no pattern-rule markers");
|
|
35
|
+
}
|
|
36
|
+
if (rules.length === 0)
|
|
37
|
+
return source;
|
|
38
|
+
const literal = JSON.stringify(rules);
|
|
39
|
+
return source.slice(0, start + START_MARKER.length) + literal + source.slice(end);
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** One pattern redirect as the edge function reads it: regex, target with $1..$9, status, excluded paths. */
|
|
5
|
+
export type EdgeRedirectRule = { p: string; t: string; s: number; x: string[] };
|
|
6
|
+
|
|
7
|
+
const START_MARKER = "/*PAPYRUS_PATTERN_RULES*/";
|
|
8
|
+
const END_MARKER = "/*END_PAPYRUS_PATTERN_RULES*/";
|
|
9
|
+
|
|
10
|
+
type ProjectionRedirects = { patterns?: { pattern: string; to: string; status?: number; except?: string[] }[] };
|
|
11
|
+
type ProjectionSettings = { urls?: { style?: string }; redirects?: ProjectionRedirects };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The pattern rules a site declares in projection.json: its `redirects.patterns`, plus, for
|
|
15
|
+
* `urls.style: "directory"`, `/x.html` to `/x/`. Mirrors `StandardSettings.edge_rules` in Python.
|
|
16
|
+
*/
|
|
17
|
+
export function edgeRulesFromSettings(settings: ProjectionSettings): EdgeRedirectRule[] {
|
|
18
|
+
const rules = (settings.redirects?.patterns ?? []).map((rule) => ({
|
|
19
|
+
p: rule.pattern,
|
|
20
|
+
t: rule.to,
|
|
21
|
+
s: rule.status ?? 301,
|
|
22
|
+
x: rule.except ?? [],
|
|
23
|
+
}));
|
|
24
|
+
if (settings.urls?.style === "directory") {
|
|
25
|
+
rules.push({ p: "^/(.+)\\.html$", t: "/$1/", s: 301, x: ["/404.html"] });
|
|
26
|
+
}
|
|
27
|
+
return rules;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readEdgeRules(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeRedirectRule[] {
|
|
31
|
+
if (staticDir === undefined) return [];
|
|
32
|
+
const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
|
|
33
|
+
if (!existsSync(settingsPath)) return [];
|
|
34
|
+
return edgeRulesFromSettings(JSON.parse(readFileSync(settingsPath, "utf8")) as ProjectionSettings);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The CloudFront Function source with the site's pattern rules spliced in; the source unchanged when there are none. */
|
|
38
|
+
export function compileRedirectFunctionSource(source: string, rules: EdgeRedirectRule[]): string {
|
|
39
|
+
const start = source.indexOf(START_MARKER);
|
|
40
|
+
const end = source.indexOf(END_MARKER);
|
|
41
|
+
if (start === -1 || end === -1 || end < start) {
|
|
42
|
+
throw new Error("redirect function source has no pattern-rule markers");
|
|
43
|
+
}
|
|
44
|
+
if (rules.length === 0) return source;
|
|
45
|
+
const literal = JSON.stringify(rules);
|
|
46
|
+
return source.slice(0, start + START_MARKER.length) + literal + source.slice(end);
|
|
47
|
+
}
|
|
@@ -7,11 +7,13 @@ import { SqsDestination } from "aws-cdk-lib/aws-lambda-destinations";
|
|
|
7
7
|
import * as route53 from "aws-cdk-lib/aws-route53";
|
|
8
8
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
9
9
|
import * as sqs from "aws-cdk-lib/aws-sqs";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
10
11
|
import { dirname, resolve } from "node:path";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { assertValidPublicationConfig } from "../site-backend-config.js";
|
|
13
14
|
import { addRenditionsFunction } from "./renditions-function.js";
|
|
14
15
|
import { RENDITIONS_FUNCTION_NAME_ENV } from "./renditions-settings.js";
|
|
16
|
+
import { compileRedirectFunctionSource, readEdgeRules } from "./redirect-rules.js";
|
|
15
17
|
import { addInvalidationPathAlarm } from "./invalidation-alarm.js";
|
|
16
18
|
import { distributionIdFromArn, s3ProjectionEnvironment, s3ProjectionPolicyStatements } from "./s3-projection-settings.js";
|
|
17
19
|
const redirectFunctionSourcePath = resolve(dirname(fileURLToPath(import.meta.url)), "redirect-and-index.js");
|
|
@@ -70,7 +72,7 @@ export function addS3ProjectionPublication(scope, props, media) {
|
|
|
70
72
|
const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
|
|
71
73
|
const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
|
|
72
74
|
runtime: cloudfront.FunctionRuntime.JS_2_0,
|
|
73
|
-
code: cloudfront.FunctionCode.
|
|
75
|
+
code: cloudfront.FunctionCode.fromInline(compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeRules(props.staticDir))),
|
|
74
76
|
keyValueStore,
|
|
75
77
|
});
|
|
76
78
|
const deadLetterQueue = new sqs.Queue(scope, "ProjectionDeadLetterQueue", {
|
|
@@ -10,11 +10,13 @@ import * as route53 from "aws-cdk-lib/aws-route53";
|
|
|
10
10
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
11
11
|
import * as sqs from "aws-cdk-lib/aws-sqs";
|
|
12
12
|
import type { Construct } from "constructs";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
13
14
|
import { dirname, resolve } from "node:path";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
import { assertValidPublicationConfig, type PapyrusPublicationConfig } from "../site-backend-config";
|
|
16
17
|
import { addRenditionsFunction, type RenditionsFunction } from "./renditions-function";
|
|
17
18
|
import { RENDITIONS_FUNCTION_NAME_ENV, type RenditionsPlan } from "./renditions-settings";
|
|
19
|
+
import { compileRedirectFunctionSource, readEdgeRules } from "./redirect-rules";
|
|
18
20
|
import { addInvalidationPathAlarm } from "./invalidation-alarm";
|
|
19
21
|
import { distributionIdFromArn, s3ProjectionEnvironment, s3ProjectionPolicyStatements } from "./s3-projection-settings";
|
|
20
22
|
|
|
@@ -112,7 +114,9 @@ export function addS3ProjectionPublication(
|
|
|
112
114
|
const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
|
|
113
115
|
const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
|
|
114
116
|
runtime: cloudfront.FunctionRuntime.JS_2_0,
|
|
115
|
-
code: cloudfront.FunctionCode.
|
|
117
|
+
code: cloudfront.FunctionCode.fromInline(
|
|
118
|
+
compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeRules(props.staticDir)),
|
|
119
|
+
),
|
|
116
120
|
keyValueStore,
|
|
117
121
|
});
|
|
118
122
|
const deadLetterQueue = new sqs.Queue(scope, "ProjectionDeadLetterQueue", {
|
package/amplify/site-backend.js
CHANGED
|
@@ -20,6 +20,8 @@ import { addInvalidationPathAlarm } from "./publication/invalidation-alarm.js";
|
|
|
20
20
|
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth, siteAuthRedirectUrls } from "./auth/resource.js";
|
|
21
21
|
import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client.js";
|
|
22
22
|
import { defineSiteData } from "./data/resource.js";
|
|
23
|
+
import { editorialChecks } from "./functions/editorial-checks/resource.js";
|
|
24
|
+
import { wireEditorialChecks } from "./functions/editorial-checks/wire.js";
|
|
23
25
|
import { resolvePlugins } from "./plugins/registry.js";
|
|
24
26
|
import { wirePlugins } from "./plugins/wire.js";
|
|
25
27
|
import { assignmentAction } from "./functions/assignment-action/resource.js";
|
|
@@ -89,6 +91,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
89
91
|
auth: defineSiteAuth(authConfig),
|
|
90
92
|
categoryAction,
|
|
91
93
|
contentActions,
|
|
94
|
+
editorialChecks,
|
|
92
95
|
data: defineSiteData(pluginDefinitions),
|
|
93
96
|
knowledgeQuery,
|
|
94
97
|
manageUserRole,
|
|
@@ -589,6 +592,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
589
592
|
: null,
|
|
590
593
|
},
|
|
591
594
|
});
|
|
595
|
+
wireEditorialChecks(backend);
|
|
592
596
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
593
597
|
return backend;
|
|
594
598
|
}
|
package/amplify/site-backend.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { addInvalidationPathAlarm } from "./publication/invalidation-alarm";
|
|
|
27
27
|
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth, siteAuthRedirectUrls } from "./auth/resource";
|
|
28
28
|
import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client";
|
|
29
29
|
import { defineSiteData } from "./data/resource";
|
|
30
|
+
import { editorialChecks } from "./functions/editorial-checks/resource";
|
|
31
|
+
import { wireEditorialChecks } from "./functions/editorial-checks/wire";
|
|
30
32
|
import { resolvePlugins } from "./plugins/registry";
|
|
31
33
|
import { wirePlugins } from "./plugins/wire";
|
|
32
34
|
import { assignmentAction } from "./functions/assignment-action/resource";
|
|
@@ -110,6 +112,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
110
112
|
auth: defineSiteAuth(authConfig),
|
|
111
113
|
categoryAction,
|
|
112
114
|
contentActions,
|
|
115
|
+
editorialChecks,
|
|
113
116
|
data: defineSiteData(pluginDefinitions),
|
|
114
117
|
knowledgeQuery,
|
|
115
118
|
manageUserRole,
|
|
@@ -732,6 +735,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
732
735
|
: null,
|
|
733
736
|
},
|
|
734
737
|
});
|
|
738
|
+
wireEditorialChecks(backend);
|
|
735
739
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
736
740
|
|
|
737
741
|
return backend;
|