@anthusai/papyrus 2.0.0-next.40 → 2.0.0-next.41
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 +1 -0
- package/amplify/data/resource.ts +1 -0
- package/amplify/data/schema.js +1 -0
- package/amplify/data/schema.ts +1 -0
- package/amplify/functions/assignment-action/handler.ts +1 -1
- package/amplify/functions/content-actions/handler.py +3 -0
- package/amplify/functions/shared/editorial-context.ts +9 -3
- package/amplify/plugins/reader-steering/config.js +110 -0
- package/amplify/plugins/reader-steering/config.ts +153 -0
- package/amplify/plugins/reader-steering/function/handler.py +544 -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 +103 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +122 -0
- package/amplify/plugins/reader-steering/schema.js +40 -0
- package/amplify/plugins/reader-steering/schema.ts +42 -0
- package/amplify/plugins/registry.js +2 -0
- package/amplify/plugins/registry.ts +2 -0
- package/amplify/plugins/types.ts +3 -0
- package/amplify/plugins/wire.js +1 -0
- package/amplify/plugins/wire.ts +1 -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/retired/route.ts +9 -0
- package/app/api/reader/vote/route.ts +9 -0
- package/components/reference-cyclotron-priming.tsx +54 -15
- package/infra/amplify-app-shell.js +15 -1
- package/infra/amplify-app-shell.ts +17 -1
- package/infra/site-config.js +12 -2
- package/infra/site-config.ts +19 -2
- package/lib/editorial-checks.ts +3 -1
- package/lib/prospectus-session.ts +1 -0
- package/lib/prospectus-sign-in-routes.ts +2 -2
- package/lib/prospectus-site-identity.ts +24 -1
- package/lib/reader-api.ts +143 -0
- package/lib/reader-retirement-route.ts +89 -0
- package/lib/reader-subject.md +29 -0
- package/lib/reader-subject.ts +53 -0
- package/package.json +9 -1
- package/routes.manifest.json +56 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ArnPrincipal, Effect, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
2
|
+
import { Duration, Stack } from "aws-cdk-lib";
|
|
3
|
+
import { Rule, RuleTargetInput, Schedule } from "aws-cdk-lib/aws-events";
|
|
4
|
+
import { LambdaFunction as LambdaFunctionTarget } from "aws-cdk-lib/aws-events-targets";
|
|
5
|
+
import { resolveReaderSteeringConfig } from "./config.js";
|
|
6
|
+
import { readerActions } from "./function/resource.js";
|
|
7
|
+
import { readerSteeringSchemaDefinitions } from "./schema.js";
|
|
8
|
+
export { resolveReaderSteeringConfig };
|
|
9
|
+
export const READER_STEERING_OUTPUT_KEY = "readerSteering";
|
|
10
|
+
export const SNAPSHOT_SCHEDULE_PREFIX = "papyrus-reader-snapshot-";
|
|
11
|
+
export const READER_SNAPSHOT_TASK = { projectionTask: { task: "reader-snapshot" } };
|
|
12
|
+
export const readerSteeringManifest = {
|
|
13
|
+
name: "reader-steering",
|
|
14
|
+
contract: 1,
|
|
15
|
+
models: ["ReaderProfile", "ReaderHandle", "ReaderVote"],
|
|
16
|
+
mutations: [],
|
|
17
|
+
guest: { mutations: [] },
|
|
18
|
+
newsroomPanels: [],
|
|
19
|
+
staticAssets: ["_plugins/papyrus-reader@1.js", "_plugins/papyrus-reader@1.css"],
|
|
20
|
+
};
|
|
21
|
+
/** What the reader-actions function reads from `PAPYRUS_READER_SETTINGS`. */
|
|
22
|
+
export function readerActionsSettings(config, allowInterimSubjects) {
|
|
23
|
+
return {
|
|
24
|
+
dimensions: config.dimensions,
|
|
25
|
+
limits: config.limits,
|
|
26
|
+
reservedHandles: config.reservedHandles,
|
|
27
|
+
allowInterimSubjects,
|
|
28
|
+
snapshotDebounceSeconds: 60,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** What the `reader-snapshot` projection task reads from `PAPYRUS_READER_SNAPSHOT_SETTINGS`. */
|
|
32
|
+
export function readerSnapshotSettings(config) {
|
|
33
|
+
return {
|
|
34
|
+
dimensions: config.dimensions,
|
|
35
|
+
karma: config.karma,
|
|
36
|
+
ranking: config.ranking,
|
|
37
|
+
hypeMinimum: config.snapshot.hypeMinimum,
|
|
38
|
+
...(config.hypeGap ? { hypeGap: config.hypeGap } : {}),
|
|
39
|
+
retirementPoll: config.retirementPoll,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function interimSubjectsAllowedAtSynth(environment = process.env) {
|
|
43
|
+
return ["development", "test"].includes((environment.SITE_ENV ?? "").trim()) && environment.NODE_ENV !== "production";
|
|
44
|
+
}
|
|
45
|
+
export const readerSteeringPlugin = {
|
|
46
|
+
manifest: readerSteeringManifest,
|
|
47
|
+
resolveConfig: resolveReaderSteeringConfig,
|
|
48
|
+
functions: () => ({ readerActions }),
|
|
49
|
+
schemaDefinitions: readerSteeringSchemaDefinitions,
|
|
50
|
+
publicSettings: (config) => ({ contract: readerSteeringManifest.contract, dimensions: config.dimensions }),
|
|
51
|
+
wire(backend, config, context) {
|
|
52
|
+
const lambda = backend.readerActions.resources.lambda;
|
|
53
|
+
const tables = backend.data.resources.tables;
|
|
54
|
+
const contentActions = context.contentActionsLambda;
|
|
55
|
+
for (const [variable, model] of [
|
|
56
|
+
["PAPYRUS_READER_PROFILE_TABLE", "ReaderProfile"],
|
|
57
|
+
["PAPYRUS_READER_HANDLE_TABLE", "ReaderHandle"],
|
|
58
|
+
["PAPYRUS_READER_VOTE_TABLE", "ReaderVote"],
|
|
59
|
+
]) {
|
|
60
|
+
tables[model].grantReadWriteData(lambda);
|
|
61
|
+
lambda.addEnvironment(variable, tables[model].tableName);
|
|
62
|
+
contentActions.addEnvironment(variable, tables[model].tableName);
|
|
63
|
+
}
|
|
64
|
+
tables.PublishedItem.grantReadData(lambda);
|
|
65
|
+
lambda.addEnvironment("PAPYRUS_READER_PUBLISHED_ITEM_TABLE", tables.PublishedItem.tableName);
|
|
66
|
+
tables.ReaderVote.grantReadData(contentActions);
|
|
67
|
+
tables.ReaderProfile.grantReadWriteData(contentActions);
|
|
68
|
+
lambda.addEnvironment("PAPYRUS_READER_SETTINGS", JSON.stringify(readerActionsSettings(config, interimSubjectsAllowedAtSynth())));
|
|
69
|
+
if (config.reservedConcurrency !== undefined) {
|
|
70
|
+
lambda.node.defaultChild.addPropertyOverride("ReservedConcurrentExecutions", config.reservedConcurrency);
|
|
71
|
+
}
|
|
72
|
+
// Trailing-edge debounce: the first write in a window creates a one-off EventBridge Scheduler schedule at the end of
|
|
73
|
+
// the window (docs/reader-steering.md). The schedule invokes content-actions as this role; reader-actions may create
|
|
74
|
+
// schedules named for this feature, and pass exactly this role.
|
|
75
|
+
const schedulerRole = new Role(Stack.of(lambda), "ReaderSnapshotSchedulerRole", {
|
|
76
|
+
assumedBy: new ServicePrincipal("scheduler.amazonaws.com"),
|
|
77
|
+
description: "Lets one-off schedules created by reader-actions invoke the reader-snapshot run.",
|
|
78
|
+
});
|
|
79
|
+
contentActions.grantInvoke(schedulerRole);
|
|
80
|
+
const stack = Stack.of(lambda); // the role lives with the function, so the stacks keep their existing one-way dependency
|
|
81
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
82
|
+
effect: Effect.ALLOW,
|
|
83
|
+
actions: ["scheduler:CreateSchedule"],
|
|
84
|
+
resources: [`arn:${stack.partition}:scheduler:${stack.region}:${stack.account}:schedule/default/${SNAPSHOT_SCHEDULE_PREFIX}*`],
|
|
85
|
+
}));
|
|
86
|
+
lambda.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: ["iam:PassRole"], resources: [schedulerRole.roleArn] }));
|
|
87
|
+
lambda.addEnvironment("PAPYRUS_READER_SNAPSHOT_FUNCTION_ARN", contentActions.functionArn);
|
|
88
|
+
lambda.addEnvironment("PAPYRUS_READER_SCHEDULER_ROLE_ARN", schedulerRole.roleArn);
|
|
89
|
+
contentActions.addEnvironment("PAPYRUS_READER_SNAPSHOT_SETTINGS", JSON.stringify(readerSnapshotSettings(config)));
|
|
90
|
+
lambda.addPermission("InvokeFromCmsCompute", {
|
|
91
|
+
principal: new ArnPrincipal(config.computeRoleArn),
|
|
92
|
+
action: "lambda:InvokeFunction",
|
|
93
|
+
});
|
|
94
|
+
if (config.snapshot.schedule) {
|
|
95
|
+
new Rule(context.createStack("reader-steering-schedule"), "ReaderSnapshotSchedule", {
|
|
96
|
+
description: "Rebuilds the reader snapshot (time decay, Rising) and polls nothing else.",
|
|
97
|
+
schedule: Schedule.rate(Duration.hours(config.snapshot.intervalHours)),
|
|
98
|
+
targets: [new LambdaFunctionTarget(contentActions, { event: RuleTargetInput.fromObject(READER_SNAPSHOT_TASK), retryAttempts: 0 })],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
backend.addOutput({ custom: { [READER_STEERING_OUTPUT_KEY]: { actionsFunction: lambda.functionName } } });
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ArnPrincipal, Effect, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
2
|
+
import { Duration, Stack } from "aws-cdk-lib";
|
|
3
|
+
import type { ITable } from "aws-cdk-lib/aws-dynamodb";
|
|
4
|
+
import { Rule, RuleTargetInput, Schedule } from "aws-cdk-lib/aws-events";
|
|
5
|
+
import { LambdaFunction as LambdaFunctionTarget } from "aws-cdk-lib/aws-events-targets";
|
|
6
|
+
import { CfnFunction, type Function as LambdaFunction } from "aws-cdk-lib/aws-lambda";
|
|
7
|
+
import type { PapyrusPlugin, PluginManifest, PluginWiringContext } from "../types";
|
|
8
|
+
import { resolveReaderSteeringConfig, type ReaderSteeringConfig } from "./config";
|
|
9
|
+
import { readerActions } from "./function/resource";
|
|
10
|
+
import { readerSteeringSchemaDefinitions } from "./schema";
|
|
11
|
+
|
|
12
|
+
export { resolveReaderSteeringConfig };
|
|
13
|
+
export type { ReaderSteeringConfig };
|
|
14
|
+
|
|
15
|
+
export const READER_STEERING_OUTPUT_KEY = "readerSteering";
|
|
16
|
+
export const SNAPSHOT_SCHEDULE_PREFIX = "papyrus-reader-snapshot-";
|
|
17
|
+
export const READER_SNAPSHOT_TASK = { projectionTask: { task: "reader-snapshot" } };
|
|
18
|
+
|
|
19
|
+
export const readerSteeringManifest: PluginManifest = {
|
|
20
|
+
name: "reader-steering",
|
|
21
|
+
contract: 1,
|
|
22
|
+
models: ["ReaderProfile", "ReaderHandle", "ReaderVote"],
|
|
23
|
+
mutations: [],
|
|
24
|
+
guest: { mutations: [] },
|
|
25
|
+
newsroomPanels: [],
|
|
26
|
+
staticAssets: ["_plugins/papyrus-reader@1.js", "_plugins/papyrus-reader@1.css"],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** What the reader-actions function reads from `PAPYRUS_READER_SETTINGS`. */
|
|
30
|
+
export function readerActionsSettings(config: ReaderSteeringConfig, allowInterimSubjects: boolean) {
|
|
31
|
+
return {
|
|
32
|
+
dimensions: config.dimensions,
|
|
33
|
+
limits: config.limits,
|
|
34
|
+
reservedHandles: config.reservedHandles,
|
|
35
|
+
allowInterimSubjects,
|
|
36
|
+
snapshotDebounceSeconds: 60,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What the `reader-snapshot` projection task reads from `PAPYRUS_READER_SNAPSHOT_SETTINGS`. */
|
|
41
|
+
export function readerSnapshotSettings(config: ReaderSteeringConfig) {
|
|
42
|
+
return {
|
|
43
|
+
dimensions: config.dimensions,
|
|
44
|
+
karma: config.karma,
|
|
45
|
+
ranking: config.ranking,
|
|
46
|
+
hypeMinimum: config.snapshot.hypeMinimum,
|
|
47
|
+
...(config.hypeGap ? { hypeGap: config.hypeGap } : {}),
|
|
48
|
+
retirementPoll: config.retirementPoll,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function interimSubjectsAllowedAtSynth(environment: Record<string, string | undefined> = process.env): boolean {
|
|
53
|
+
return ["development", "test"].includes((environment.SITE_ENV ?? "").trim()) && environment.NODE_ENV !== "production";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerActions: typeof readerActions }> = {
|
|
57
|
+
manifest: readerSteeringManifest,
|
|
58
|
+
resolveConfig: resolveReaderSteeringConfig,
|
|
59
|
+
functions: () => ({ readerActions }),
|
|
60
|
+
schemaDefinitions: readerSteeringSchemaDefinitions,
|
|
61
|
+
publicSettings: (config) => ({ contract: readerSteeringManifest.contract, dimensions: config.dimensions }),
|
|
62
|
+
wire(backend, config, context: PluginWiringContext) {
|
|
63
|
+
const lambda = backend.readerActions.resources.lambda as LambdaFunction;
|
|
64
|
+
const tables = backend.data.resources.tables as Record<string, ITable>;
|
|
65
|
+
const contentActions = context.contentActionsLambda;
|
|
66
|
+
|
|
67
|
+
for (const [variable, model] of [
|
|
68
|
+
["PAPYRUS_READER_PROFILE_TABLE", "ReaderProfile"],
|
|
69
|
+
["PAPYRUS_READER_HANDLE_TABLE", "ReaderHandle"],
|
|
70
|
+
["PAPYRUS_READER_VOTE_TABLE", "ReaderVote"],
|
|
71
|
+
] as const) {
|
|
72
|
+
tables[model].grantReadWriteData(lambda);
|
|
73
|
+
lambda.addEnvironment(variable, tables[model].tableName);
|
|
74
|
+
contentActions.addEnvironment(variable, tables[model].tableName);
|
|
75
|
+
}
|
|
76
|
+
tables.PublishedItem.grantReadData(lambda);
|
|
77
|
+
lambda.addEnvironment("PAPYRUS_READER_PUBLISHED_ITEM_TABLE", tables.PublishedItem.tableName);
|
|
78
|
+
tables.ReaderVote.grantReadData(contentActions);
|
|
79
|
+
tables.ReaderProfile.grantReadWriteData(contentActions);
|
|
80
|
+
|
|
81
|
+
lambda.addEnvironment("PAPYRUS_READER_SETTINGS", JSON.stringify(readerActionsSettings(config, interimSubjectsAllowedAtSynth())));
|
|
82
|
+
if (config.reservedConcurrency !== undefined) {
|
|
83
|
+
(lambda.node.defaultChild as CfnFunction).addPropertyOverride("ReservedConcurrentExecutions", config.reservedConcurrency);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Trailing-edge debounce: the first write in a window creates a one-off EventBridge Scheduler schedule at the end of
|
|
87
|
+
// the window (docs/reader-steering.md). The schedule invokes content-actions as this role; reader-actions may create
|
|
88
|
+
// schedules named for this feature, and pass exactly this role.
|
|
89
|
+
const schedulerRole = new Role(Stack.of(lambda), "ReaderSnapshotSchedulerRole", {
|
|
90
|
+
assumedBy: new ServicePrincipal("scheduler.amazonaws.com"),
|
|
91
|
+
description: "Lets one-off schedules created by reader-actions invoke the reader-snapshot run.",
|
|
92
|
+
});
|
|
93
|
+
contentActions.grantInvoke(schedulerRole);
|
|
94
|
+
const stack = Stack.of(lambda); // the role lives with the function, so the stacks keep their existing one-way dependency
|
|
95
|
+
lambda.addToRolePolicy(
|
|
96
|
+
new PolicyStatement({
|
|
97
|
+
effect: Effect.ALLOW,
|
|
98
|
+
actions: ["scheduler:CreateSchedule"],
|
|
99
|
+
resources: [`arn:${stack.partition}:scheduler:${stack.region}:${stack.account}:schedule/default/${SNAPSHOT_SCHEDULE_PREFIX}*`],
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
lambda.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: ["iam:PassRole"], resources: [schedulerRole.roleArn] }));
|
|
103
|
+
lambda.addEnvironment("PAPYRUS_READER_SNAPSHOT_FUNCTION_ARN", contentActions.functionArn);
|
|
104
|
+
lambda.addEnvironment("PAPYRUS_READER_SCHEDULER_ROLE_ARN", schedulerRole.roleArn);
|
|
105
|
+
contentActions.addEnvironment("PAPYRUS_READER_SNAPSHOT_SETTINGS", JSON.stringify(readerSnapshotSettings(config)));
|
|
106
|
+
|
|
107
|
+
lambda.addPermission("InvokeFromCmsCompute", {
|
|
108
|
+
principal: new ArnPrincipal(config.computeRoleArn),
|
|
109
|
+
action: "lambda:InvokeFunction",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (config.snapshot.schedule) {
|
|
113
|
+
new Rule(context.createStack("reader-steering-schedule"), "ReaderSnapshotSchedule", {
|
|
114
|
+
description: "Rebuilds the reader snapshot (time decay, Rising) and polls nothing else.",
|
|
115
|
+
schedule: Schedule.rate(Duration.hours(config.snapshot.intervalHours)),
|
|
116
|
+
targets: [new LambdaFunctionTarget(contentActions, { event: RuleTargetInput.fromObject(READER_SNAPSHOT_TASK), retryAttempts: 0 })],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
backend.addOutput({ custom: { [READER_STEERING_OUTPUT_KEY]: { actionsFunction: lambda.functionName } } });
|
|
121
|
+
},
|
|
122
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
/**
|
|
3
|
+
* Staff and IAM only: no guest, API key or reader rule on any model. The reader-actions function writes the
|
|
4
|
+
* tables directly with its own role, and editors may list them; admins may delete. `id` is the lookup key:
|
|
5
|
+
* the siteSubject for a profile, the lowercase handle for a claim and `subjectKey#readerId#dimension` for a vote.
|
|
6
|
+
*/
|
|
7
|
+
const staffOnly = (allow) => [allow.group("editor").to(["read"]), allow.group("admin").to(["read", "delete"])];
|
|
8
|
+
export function readerSteeringSchemaDefinitions() {
|
|
9
|
+
return {
|
|
10
|
+
ReaderProfile: a
|
|
11
|
+
.model({
|
|
12
|
+
id: a.id().required(),
|
|
13
|
+
siteSubject: a.string().required(),
|
|
14
|
+
readerId: a.string().required(),
|
|
15
|
+
handle: a.string(),
|
|
16
|
+
state: a.string().required(),
|
|
17
|
+
karma: a.integer(),
|
|
18
|
+
})
|
|
19
|
+
.disableOperations(["subscriptions"])
|
|
20
|
+
.authorization(staffOnly),
|
|
21
|
+
ReaderHandle: a
|
|
22
|
+
.model({
|
|
23
|
+
id: a.id().required(),
|
|
24
|
+
readerId: a.string().required(),
|
|
25
|
+
})
|
|
26
|
+
.disableOperations(["subscriptions"])
|
|
27
|
+
.authorization(staffOnly),
|
|
28
|
+
ReaderVote: a
|
|
29
|
+
.model({
|
|
30
|
+
id: a.id().required(),
|
|
31
|
+
subjectKey: a.string().required(),
|
|
32
|
+
readerId: a.string().required(),
|
|
33
|
+
dimension: a.string().required(),
|
|
34
|
+
value: a.integer().required(),
|
|
35
|
+
voterRole: a.string().required(),
|
|
36
|
+
})
|
|
37
|
+
.disableOperations(["subscriptions"])
|
|
38
|
+
.authorization(staffOnly),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Staff and IAM only: no guest, API key or reader rule on any model. The reader-actions function writes the
|
|
5
|
+
* tables directly with its own role, and editors may list them; admins may delete. `id` is the lookup key:
|
|
6
|
+
* the siteSubject for a profile, the lowercase handle for a claim and `subjectKey#readerId#dimension` for a vote.
|
|
7
|
+
*/
|
|
8
|
+
const staffOnly = (allow: any) => [allow.group("editor").to(["read"]), allow.group("admin").to(["read", "delete"])];
|
|
9
|
+
|
|
10
|
+
export function readerSteeringSchemaDefinitions() {
|
|
11
|
+
return {
|
|
12
|
+
ReaderProfile: a
|
|
13
|
+
.model({
|
|
14
|
+
id: a.id().required(),
|
|
15
|
+
siteSubject: a.string().required(),
|
|
16
|
+
readerId: a.string().required(),
|
|
17
|
+
handle: a.string(),
|
|
18
|
+
state: a.string().required(),
|
|
19
|
+
karma: a.integer(),
|
|
20
|
+
})
|
|
21
|
+
.disableOperations(["subscriptions"])
|
|
22
|
+
.authorization(staffOnly),
|
|
23
|
+
ReaderHandle: a
|
|
24
|
+
.model({
|
|
25
|
+
id: a.id().required(),
|
|
26
|
+
readerId: a.string().required(),
|
|
27
|
+
})
|
|
28
|
+
.disableOperations(["subscriptions"])
|
|
29
|
+
.authorization(staffOnly),
|
|
30
|
+
ReaderVote: a
|
|
31
|
+
.model({
|
|
32
|
+
id: a.id().required(),
|
|
33
|
+
subjectKey: a.string().required(),
|
|
34
|
+
readerId: a.string().required(),
|
|
35
|
+
dimension: a.string().required(),
|
|
36
|
+
value: a.integer().required(),
|
|
37
|
+
voterRole: a.string().required(),
|
|
38
|
+
})
|
|
39
|
+
.disableOperations(["subscriptions"])
|
|
40
|
+
.authorization(staffOnly),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { readerSteeringPlugin } from "./reader-steering/papyrus.plugin.js";
|
|
1
2
|
import { signupPlugin } from "./signup/papyrus.plugin.js";
|
|
2
3
|
/** Plugins are packaged with Papyrus and pinned by its version: there is no registry to fetch from. */
|
|
3
4
|
export const KNOWN_PLUGINS = {
|
|
5
|
+
"reader-steering": readerSteeringPlugin,
|
|
4
6
|
signup: signupPlugin,
|
|
5
7
|
};
|
|
6
8
|
export function resolvePlugins(requests) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { PapyrusPlugin, PluginRequest } from "./types";
|
|
2
|
+
import { readerSteeringPlugin } from "./reader-steering/papyrus.plugin";
|
|
2
3
|
import { signupPlugin } from "./signup/papyrus.plugin";
|
|
3
4
|
|
|
4
5
|
/** Plugins are packaged with Papyrus and pinned by its version: there is no registry to fetch from. */
|
|
5
6
|
export const KNOWN_PLUGINS: Record<string, PapyrusPlugin<any, any>> = {
|
|
7
|
+
"reader-steering": readerSteeringPlugin,
|
|
6
8
|
signup: signupPlugin,
|
|
7
9
|
};
|
|
8
10
|
|
package/amplify/plugins/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Function as LambdaFunction } from "aws-cdk-lib/aws-lambda";
|
|
1
2
|
import type { Construct } from "constructs";
|
|
2
3
|
|
|
3
4
|
/** How a site asks for a plugin in `papyrus.config.ts`: `plugins: ["signup"]` or `plugins: [{ name: "signup", forms: ["news"] }]`. */
|
|
@@ -22,6 +23,8 @@ export type PluginWiringContext = {
|
|
|
22
23
|
createStack: (name: string) => Construct;
|
|
23
24
|
graphqlApiId: string;
|
|
24
25
|
graphqlEndpoint: string;
|
|
26
|
+
/** The site's `content-actions` function: it runs the projection tasks a plugin may add (for example `reader-snapshot`). */
|
|
27
|
+
contentActionsLambda: LambdaFunction;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
export type PapyrusPlugin<Config, Functions = unknown> = {
|
package/amplify/plugins/wire.js
CHANGED
|
@@ -12,6 +12,7 @@ export function wirePlugins(backend, plugins, contentActionsLambda) {
|
|
|
12
12
|
createStack: (name) => backend.createStack(name),
|
|
13
13
|
graphqlApiId: graphqlApi.apiId,
|
|
14
14
|
graphqlEndpoint: backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
|
|
15
|
+
contentActionsLambda,
|
|
15
16
|
};
|
|
16
17
|
for (const { plugin, config } of plugins)
|
|
17
18
|
plugin.wire(backend, config, context);
|
package/amplify/plugins/wire.ts
CHANGED
|
@@ -15,6 +15,7 @@ export function wirePlugins(backend: any, plugins: ResolvedPlugin[], contentActi
|
|
|
15
15
|
createStack: (name: string) => backend.createStack(name),
|
|
16
16
|
graphqlApiId: graphqlApi.apiId as string,
|
|
17
17
|
graphqlEndpoint: backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl as string,
|
|
18
|
+
contentActionsLambda,
|
|
18
19
|
};
|
|
19
20
|
for (const { plugin, config } of plugins) plugin.wire(backend, config, context);
|
|
20
21
|
contentActionsLambda.addEnvironment(PLUGINS_ENVIRONMENT_VARIABLE, plugins.map(({ plugin }) => plugin.manifest.name).join(","));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "handle");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "me");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "rating");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRetiredSubjects } from "../../../../lib/reader-retirement-route";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleReaderRetiredSubjects(request);
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "vote");
|
|
9
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useId, useRef, useState, type KeyboardEvent } from "react";
|
|
4
4
|
import { primingStateText, type PrimingRequestView } from "../lib/relevance-status";
|
|
5
5
|
|
|
6
6
|
export type PrimingFormInput = { url: string; label: string; explanation: string };
|
|
@@ -23,6 +23,19 @@ export function ReferenceCyclotronPriming({
|
|
|
23
23
|
const [explanation, setExplanation] = useState("");
|
|
24
24
|
const [message, setMessage] = useState<string | null>(null);
|
|
25
25
|
|
|
26
|
+
const ratingLabelId = useId();
|
|
27
|
+
const buttons = useRef<Array<HTMLButtonElement | null>>([]);
|
|
28
|
+
|
|
29
|
+
function moveSelection(event: KeyboardEvent<HTMLButtonElement>, index: number) {
|
|
30
|
+
const forward = event.key === "ArrowRight" || event.key === "ArrowDown";
|
|
31
|
+
const backward = event.key === "ArrowLeft" || event.key === "ArrowUp";
|
|
32
|
+
if (!forward && !backward) return;
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
const next = (index + (forward ? 1 : -1) + labels.length) % labels.length;
|
|
35
|
+
setLabel(labels[next]);
|
|
36
|
+
buttons.current[next]?.focus();
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
async function submit() {
|
|
27
40
|
setMessage("Adding…");
|
|
28
41
|
try {
|
|
@@ -38,13 +51,13 @@ export function ReferenceCyclotronPriming({
|
|
|
38
51
|
return (
|
|
39
52
|
<div className="space-y-2 text-sm" data-reference-cyclotron-priming>
|
|
40
53
|
<form
|
|
41
|
-
className="flex flex-
|
|
54
|
+
className="flex flex-col gap-3"
|
|
42
55
|
onSubmit={(event) => {
|
|
43
56
|
event.preventDefault();
|
|
44
57
|
void submit();
|
|
45
58
|
}}
|
|
46
59
|
>
|
|
47
|
-
<label className="flex
|
|
60
|
+
<label className="flex flex-col gap-1">
|
|
48
61
|
<span className="text-xs text-muted-foreground">Web page address</span>
|
|
49
62
|
<input
|
|
50
63
|
aria-label="Reference URL"
|
|
@@ -55,20 +68,46 @@ export function ReferenceCyclotronPriming({
|
|
|
55
68
|
value={url}
|
|
56
69
|
/>
|
|
57
70
|
</label>
|
|
71
|
+
<div className="flex flex-col gap-1">
|
|
72
|
+
<span className="text-xs text-muted-foreground" id={ratingLabelId}>Should Papyrus include or exclude pages like this?</span>
|
|
73
|
+
<div aria-labelledby={ratingLabelId} className="flex flex-wrap gap-2" role="radiogroup">
|
|
74
|
+
{labels.map((choice, index) => {
|
|
75
|
+
const checked = choice === label;
|
|
76
|
+
return (
|
|
77
|
+
<button
|
|
78
|
+
aria-checked={checked}
|
|
79
|
+
className={`min-w-32 flex-1 rounded border px-4 py-3 text-base font-semibold capitalize ${checked ? "border-foreground bg-foreground text-background" : "border-border bg-transparent"}`}
|
|
80
|
+
data-rating-choice={choice}
|
|
81
|
+
key={choice}
|
|
82
|
+
onClick={() => setLabel(choice)}
|
|
83
|
+
onKeyDown={(event) => moveSelection(event, index)}
|
|
84
|
+
ref={(node) => { buttons.current[index] = node; }}
|
|
85
|
+
role="radio"
|
|
86
|
+
tabIndex={checked || (!labels.includes(label) && index === 0) ? 0 : -1}
|
|
87
|
+
type="button"
|
|
88
|
+
>
|
|
89
|
+
{choice}
|
|
90
|
+
</button>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
58
95
|
<label className="flex flex-col gap-1">
|
|
59
|
-
<span className="text-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
))}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
96
|
+
<span className="text-sm font-medium">Why?</span>
|
|
97
|
+
<textarea
|
|
98
|
+
aria-label="Reason"
|
|
99
|
+
className="w-full"
|
|
100
|
+
onChange={(event) => setExplanation(event.target.value)}
|
|
101
|
+
placeholder="Why? A sentence or two helps Papyrus learn what you want."
|
|
102
|
+
rows={4}
|
|
103
|
+
value={explanation}
|
|
104
|
+
/>
|
|
105
|
+
<span className="text-xs text-muted-foreground">Optional, but a short reason helps Papyrus learn what you want.</span>
|
|
69
106
|
</label>
|
|
70
|
-
<
|
|
71
|
-
|
|
107
|
+
<div>
|
|
108
|
+
<button className="cyclotron-review-secondary" type="submit">Add reference</button>
|
|
109
|
+
</div>
|
|
110
|
+
{message ? <p className="text-xs" role="status">{message}</p> : null}
|
|
72
111
|
</form>
|
|
73
112
|
{requests.length ? (
|
|
74
113
|
<ul aria-label="Added references" className="space-y-1 text-xs text-muted-foreground">
|
|
@@ -4,7 +4,7 @@ import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
|
4
4
|
import { SecureStringParameter } from "./secure-string-parameter.js";
|
|
5
5
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
|
|
6
6
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
|
|
7
|
-
import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
7
|
+
import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveReaderPollSecretParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
8
8
|
function environmentVariables(variables) {
|
|
9
9
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
10
10
|
}
|
|
@@ -123,6 +123,20 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
123
123
|
resources: [prospectusSessionSealing.parameterArn],
|
|
124
124
|
}));
|
|
125
125
|
}
|
|
126
|
+
if (prospectusAccountsApiStageArn && config.cms.readerRetirementPoll) {
|
|
127
|
+
// Prospectus admits one caller per site, this compute role, so the scheduled snapshot run reaches the retired-subject
|
|
128
|
+
// route through the CMS route /api/reader/retired, which it authenticates with this secret.
|
|
129
|
+
const readerPollSecret = new SecureStringParameter(this, "ReaderPollSecretParameter", {
|
|
130
|
+
parameterName: resolveReaderPollSecretParameterName(config),
|
|
131
|
+
description: `Authorizes the scheduled reader snapshot run to call the retired-subject route of ${cmsAppName}`,
|
|
132
|
+
});
|
|
133
|
+
prospectusEnvironment.PAPYRUS_READER_POLL_SECRET_PARAMETER = readerPollSecret.parameterName;
|
|
134
|
+
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
135
|
+
sid: "ReadReaderPollSecret",
|
|
136
|
+
actions: ["ssm:GetParameter"],
|
|
137
|
+
resources: [readerPollSecret.parameterArn],
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
126
140
|
if (prospectusAccountsApiStageArn) {
|
|
127
141
|
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
128
142
|
sid: "InvokeProspectusAccountsAsRegisteredSite",
|
|
@@ -5,7 +5,7 @@ import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
|
5
5
|
import { SecureStringParameter } from "./secure-string-parameter";
|
|
6
6
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
|
|
7
7
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
|
|
8
|
-
import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
8
|
+
import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveReaderPollSecretParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
9
9
|
|
|
10
10
|
function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
|
|
11
11
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
@@ -136,6 +136,22 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
136
136
|
}),
|
|
137
137
|
);
|
|
138
138
|
}
|
|
139
|
+
if (prospectusAccountsApiStageArn && config.cms.readerRetirementPoll) {
|
|
140
|
+
// Prospectus admits one caller per site, this compute role, so the scheduled snapshot run reaches the retired-subject
|
|
141
|
+
// route through the CMS route /api/reader/retired, which it authenticates with this secret.
|
|
142
|
+
const readerPollSecret = new SecureStringParameter(this, "ReaderPollSecretParameter", {
|
|
143
|
+
parameterName: resolveReaderPollSecretParameterName(config),
|
|
144
|
+
description: `Authorizes the scheduled reader snapshot run to call the retired-subject route of ${cmsAppName}`,
|
|
145
|
+
});
|
|
146
|
+
prospectusEnvironment.PAPYRUS_READER_POLL_SECRET_PARAMETER = readerPollSecret.parameterName;
|
|
147
|
+
computeRole.addToPolicy(
|
|
148
|
+
new iam.PolicyStatement({
|
|
149
|
+
sid: "ReadReaderPollSecret",
|
|
150
|
+
actions: ["ssm:GetParameter"],
|
|
151
|
+
resources: [readerPollSecret.parameterArn],
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
139
155
|
if (prospectusAccountsApiStageArn) {
|
|
140
156
|
computeRole.addToPolicy(
|
|
141
157
|
new iam.PolicyStatement({
|
package/infra/site-config.js
CHANGED
|
@@ -35,7 +35,7 @@ const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
|
35
35
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
36
36
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
37
37
|
const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
|
|
38
|
-
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_SITE_ORIGIN"];
|
|
38
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_READER_POLL_SECRET_PARAMETER", "PAPYRUS_SITE_ORIGIN"];
|
|
39
39
|
/** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
|
|
40
40
|
export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
|
|
41
41
|
"GET/v1/identities/me",
|
|
@@ -63,6 +63,9 @@ function originOfRedirectUrl(value) {
|
|
|
63
63
|
export function resolveProspectusSessionSealingParameterName(config) {
|
|
64
64
|
return `/papyrus/${config.siteId}/prospectus-session-sealing`;
|
|
65
65
|
}
|
|
66
|
+
export function resolveReaderPollSecretParameterName(config) {
|
|
67
|
+
return `/papyrus/${config.siteId}/reader-poll-secret`;
|
|
68
|
+
}
|
|
66
69
|
export function resolveRevalidateSecretParameterName(config) {
|
|
67
70
|
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
68
71
|
}
|
|
@@ -222,7 +225,7 @@ function parseStagingPreview(value) {
|
|
|
222
225
|
}
|
|
223
226
|
function parseCms(value) {
|
|
224
227
|
const cmsRecord = requireObject(value, "cms");
|
|
225
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
|
|
228
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "readerRetirementPoll", "deployStackInBuild"], "cms");
|
|
226
229
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
227
230
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
228
231
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -233,6 +236,12 @@ function parseCms(value) {
|
|
|
233
236
|
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
234
237
|
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
235
238
|
}
|
|
239
|
+
if (cmsRecord.readerRetirementPoll !== undefined && typeof cmsRecord.readerRetirementPoll !== "boolean") {
|
|
240
|
+
fail("cms.readerRetirementPoll", `must be a boolean, got ${JSON.stringify(cmsRecord.readerRetirementPoll)}`);
|
|
241
|
+
}
|
|
242
|
+
if (cmsRecord.readerRetirementPoll === true && cmsRecord.prospectusAccountsApiStageArn === undefined) {
|
|
243
|
+
fail("cms.readerRetirementPoll", "requires cms.prospectusAccountsApiStageArn: the poll calls Prospectus as the site");
|
|
244
|
+
}
|
|
236
245
|
if (cmsRecord.deployStackInBuild !== undefined && typeof cmsRecord.deployStackInBuild !== "boolean") {
|
|
237
246
|
fail("cms.deployStackInBuild", `must be a boolean, got ${JSON.stringify(cmsRecord.deployStackInBuild)}`);
|
|
238
247
|
}
|
|
@@ -250,6 +259,7 @@ function parseCms(value) {
|
|
|
250
259
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
251
260
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
252
261
|
deployStackInBuild: cmsRecord.deployStackInBuild,
|
|
262
|
+
readerRetirementPoll: cmsRecord.readerRetirementPoll,
|
|
253
263
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
254
264
|
prospectusAccountsApiStageArn: optionalString(cmsRecord.prospectusAccountsApiStageArn, "cms.prospectusAccountsApiStageArn", PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN, "arn:aws:execute-api:<region>:<account>:<api id>/api"),
|
|
255
265
|
};
|