@anthusai/papyrus 2.0.0-next.73 → 2.0.0-next.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/amplify/data/resource.js +69 -0
  2. package/amplify/data/resource.ts +73 -0
  3. package/amplify/data/schema.js +68 -0
  4. package/amplify/data/schema.ts +72 -0
  5. package/amplify/functions/automation-view/handler.py +17 -0
  6. package/amplify/functions/automation-view/resource.js +24 -0
  7. package/amplify/functions/automation-view/resource.ts +34 -0
  8. package/amplify/functions/automation-view/schema.js +25 -0
  9. package/amplify/functions/automation-view/schema.ts +27 -0
  10. package/amplify/functions/automation-view/wire.js +56 -0
  11. package/amplify/functions/automation-view/wire.ts +76 -0
  12. package/amplify/plugins/reader-steering/config.js +3 -1
  13. package/amplify/plugins/reader-steering/config.ts +11 -1
  14. package/amplify/plugins/reader-steering/papyrus.plugin.js +2 -0
  15. package/amplify/plugins/reader-steering/papyrus.plugin.ts +2 -0
  16. package/amplify/site-backend.js +4 -0
  17. package/amplify/site-backend.ts +4 -0
  18. package/components/automation-panel.tsx +49 -0
  19. package/components/automation-run-now.tsx +97 -0
  20. package/components/automation-view.tsx +192 -0
  21. package/components/newsroom-ops-overview.tsx +18 -0
  22. package/components/notification-center.tsx +262 -0
  23. package/components/topic-steering-workspace.tsx +17 -3
  24. package/lib/automation-client.ts +54 -0
  25. package/lib/automation-fixture.ts +93 -0
  26. package/lib/automation-overview.ts +232 -0
  27. package/lib/review-client.ts +99 -0
  28. package/lib/review-fixture.ts +45 -0
  29. package/lib/review-requests.ts +238 -0
  30. package/package.json +11 -1
  31. package/routes.manifest.json +1 -1
@@ -1152,6 +1152,8 @@ const schema = a.schema({
1152
1152
  error: a.json(),
1153
1153
  attempt: a.integer(),
1154
1154
  newsroomFeedKey: a.string(),
1155
+ usage: a.json(),
1156
+ costUsd: a.float(),
1155
1157
  })
1156
1158
  .secondaryIndexes((index) => [
1157
1159
  index("procedureId").sortKeys(["requestedAt"]).queryField("listProcedureRunsByProcedureAndRequestedAt"),
@@ -1163,6 +1165,73 @@ const schema = a.schema({
1163
1165
  allow.group("editor").to(["read"]),
1164
1166
  allow.group(adminGroup).to(authoringOperations),
1165
1167
  ]),
1168
+ Automation: a
1169
+ .model({
1170
+ id: a.id().required(),
1171
+ title: a.string().required(),
1172
+ description: a.string(),
1173
+ enabled: a.boolean().required(),
1174
+ trigger: a.json().required(),
1175
+ procedureKey: a.string().required(),
1176
+ inputTemplate: a.json(),
1177
+ limits: a.json(),
1178
+ createdBy: a.string(),
1179
+ createdAt: a.datetime().required(),
1180
+ updatedBy: a.string(),
1181
+ updatedAt: a.datetime().required(),
1182
+ })
1183
+ .authorization((allow) => [
1184
+ allow.group("editor").to(["read", "create", "update"]),
1185
+ allow.group(adminGroup).to(authoringOperations),
1186
+ ]),
1187
+ /** A human review point raised by a step on any item (PPY-ab6a50). Review is not a story state. */
1188
+ ReviewRequest: a
1189
+ .model({
1190
+ id: a.id().required(),
1191
+ subject: a.json().required(),
1192
+ mode: a.string().required(),
1193
+ reason: a.string().required(),
1194
+ ask: a.string(),
1195
+ raisedBy: a.string().required(),
1196
+ raisedAt: a.datetime().required(),
1197
+ deadline: a.datetime(),
1198
+ state: a.string().required(),
1199
+ answer: a.json(),
1200
+ links: a.json(),
1201
+ history: a.json(),
1202
+ appliedIn: a.json(),
1203
+ updatedAt: a.datetime().required(),
1204
+ })
1205
+ .authorization((allow) => [
1206
+ allow.group("editor").to(["read", "create", "update"]),
1207
+ allow.group(adminGroup).to(authoringOperations),
1208
+ ]),
1209
+ /** One user's read mark on one review request; id is "<userKey>#<requestId>". */
1210
+ ReviewReadMark: a
1211
+ .model({
1212
+ id: a.id().required(),
1213
+ requestId: a.string().required(),
1214
+ userKey: a.string().required(),
1215
+ readAt: a.datetime().required(),
1216
+ })
1217
+ .authorization((allow) => [
1218
+ allow.groups(["editor", adminGroup]).to(["read", "create", "update"]),
1219
+ ]),
1220
+ /** Who is told about review requests: scope default, mode or automation. Changed by data, not code. */
1221
+ ReviewRecipientRule: a
1222
+ .model({
1223
+ id: a.id().required(),
1224
+ scope: a.string().required(),
1225
+ key: a.string(),
1226
+ roles: a.string().array(),
1227
+ emails: a.string().array(),
1228
+ updatedBy: a.string(),
1229
+ updatedAt: a.datetime().required(),
1230
+ })
1231
+ .authorization((allow) => [
1232
+ allow.group("editor").to(["read"]),
1233
+ allow.group(adminGroup).to(authoringOperations),
1234
+ ]),
1166
1235
  NewsroomSection: a
1167
1236
  .model({
1168
1237
  id: a.id().required(),
@@ -1247,6 +1247,8 @@ const schema = a.schema({
1247
1247
  error: a.json(),
1248
1248
  attempt: a.integer(),
1249
1249
  newsroomFeedKey: a.string(),
1250
+ usage: a.json(),
1251
+ costUsd: a.float(),
1250
1252
  })
1251
1253
  .secondaryIndexes((index) => [
1252
1254
  index("procedureId").sortKeys(["requestedAt"]).queryField("listProcedureRunsByProcedureAndRequestedAt"),
@@ -1259,6 +1261,77 @@ const schema = a.schema({
1259
1261
  allow.group(adminGroup).to(authoringOperations),
1260
1262
  ]),
1261
1263
 
1264
+ Automation: a
1265
+ .model({
1266
+ id: a.id().required(),
1267
+ title: a.string().required(),
1268
+ description: a.string(),
1269
+ enabled: a.boolean().required(),
1270
+ trigger: a.json().required(),
1271
+ procedureKey: a.string().required(),
1272
+ inputTemplate: a.json(),
1273
+ limits: a.json(),
1274
+ createdBy: a.string(),
1275
+ createdAt: a.datetime().required(),
1276
+ updatedBy: a.string(),
1277
+ updatedAt: a.datetime().required(),
1278
+ })
1279
+ .authorization((allow) => [
1280
+ allow.group("editor").to(["read", "create", "update"]),
1281
+ allow.group(adminGroup).to(authoringOperations),
1282
+ ]),
1283
+
1284
+ /** A human review point raised by a step on any item (PPY-ab6a50). Review is not a story state. */
1285
+ ReviewRequest: a
1286
+ .model({
1287
+ id: a.id().required(),
1288
+ subject: a.json().required(),
1289
+ mode: a.string().required(),
1290
+ reason: a.string().required(),
1291
+ ask: a.string(),
1292
+ raisedBy: a.string().required(),
1293
+ raisedAt: a.datetime().required(),
1294
+ deadline: a.datetime(),
1295
+ state: a.string().required(),
1296
+ answer: a.json(),
1297
+ links: a.json(),
1298
+ history: a.json(),
1299
+ appliedIn: a.json(),
1300
+ updatedAt: a.datetime().required(),
1301
+ })
1302
+ .authorization((allow) => [
1303
+ allow.group("editor").to(["read", "create", "update"]),
1304
+ allow.group(adminGroup).to(authoringOperations),
1305
+ ]),
1306
+
1307
+ /** One user's read mark on one review request; id is "<userKey>#<requestId>". */
1308
+ ReviewReadMark: a
1309
+ .model({
1310
+ id: a.id().required(),
1311
+ requestId: a.string().required(),
1312
+ userKey: a.string().required(),
1313
+ readAt: a.datetime().required(),
1314
+ })
1315
+ .authorization((allow) => [
1316
+ allow.groups(["editor", adminGroup]).to(["read", "create", "update"]),
1317
+ ]),
1318
+
1319
+ /** Who is told about review requests: scope default, mode or automation. Changed by data, not code. */
1320
+ ReviewRecipientRule: a
1321
+ .model({
1322
+ id: a.id().required(),
1323
+ scope: a.string().required(),
1324
+ key: a.string(),
1325
+ roles: a.string().array(),
1326
+ emails: a.string().array(),
1327
+ updatedBy: a.string(),
1328
+ updatedAt: a.datetime().required(),
1329
+ })
1330
+ .authorization((allow) => [
1331
+ allow.group("editor").to(["read"]),
1332
+ allow.group(adminGroup).to(authoringOperations),
1333
+ ]),
1334
+
1262
1335
  NewsroomSection: a
1263
1336
  .model({
1264
1337
  id: a.id().required(),
@@ -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 { automationViewSchemaDefinitions } from "../functions/automation-view/schema.js";
5
6
  import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema.js";
6
7
  import { contentActions } from "../functions/content-actions/resource.js";
7
8
  import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
@@ -1071,6 +1072,8 @@ function buildBaseDefinitions(includeEditions) {
1071
1072
  error: a.json(),
1072
1073
  attempt: a.integer(),
1073
1074
  newsroomFeedKey: a.string(),
1075
+ usage: a.json(),
1076
+ costUsd: a.float(),
1074
1077
  })
1075
1078
  .secondaryIndexes((index) => [
1076
1079
  index("procedureId").sortKeys(["requestedAt"]).queryField("listProcedureRunsByProcedureAndRequestedAt"),
@@ -1082,6 +1085,70 @@ function buildBaseDefinitions(includeEditions) {
1082
1085
  allow.group("editor").to(["read"]),
1083
1086
  allow.group(adminGroup).to(authoringOperations),
1084
1087
  ]),
1088
+ /** A named "when X happens, run procedure Y" (PPY-9ff95b). Step 1 mirrors the monitoring router's procedureTriggers and starts nothing itself. */
1089
+ Automation: modelWithoutSubscriptions({
1090
+ id: a.id().required(),
1091
+ title: a.string().required(),
1092
+ description: a.string(),
1093
+ enabled: a.boolean().required(),
1094
+ trigger: a.json().required(),
1095
+ procedureKey: a.string().required(),
1096
+ inputTemplate: a.json(),
1097
+ limits: a.json(),
1098
+ createdBy: a.string(),
1099
+ createdAt: a.datetime().required(),
1100
+ updatedBy: a.string(),
1101
+ updatedAt: a.datetime().required(),
1102
+ })
1103
+ .authorization((allow) => [
1104
+ allow.group("editor").to(["read", "create", "update"]),
1105
+ allow.group(adminGroup).to(authoringOperations),
1106
+ ]),
1107
+ /** A human review point raised by a step on any item (PPY-ab6a50). Review is not a story state. */
1108
+ ReviewRequest: modelWithoutSubscriptions({
1109
+ id: a.id().required(),
1110
+ subject: a.json().required(),
1111
+ mode: a.string().required(),
1112
+ reason: a.string().required(),
1113
+ ask: a.string(),
1114
+ raisedBy: a.string().required(),
1115
+ raisedAt: a.datetime().required(),
1116
+ deadline: a.datetime(),
1117
+ state: a.string().required(),
1118
+ answer: a.json(),
1119
+ links: a.json(),
1120
+ history: a.json(),
1121
+ appliedIn: a.json(),
1122
+ updatedAt: a.datetime().required(),
1123
+ })
1124
+ .authorization((allow) => [
1125
+ allow.group("editor").to(["read", "create", "update"]),
1126
+ allow.group(adminGroup).to(authoringOperations),
1127
+ ]),
1128
+ /** One user's read mark on one review request; id is "<userKey>#<requestId>". */
1129
+ ReviewReadMark: modelWithoutSubscriptions({
1130
+ id: a.id().required(),
1131
+ requestId: a.string().required(),
1132
+ userKey: a.string().required(),
1133
+ readAt: a.datetime().required(),
1134
+ })
1135
+ .authorization((allow) => [
1136
+ allow.groups(["editor", adminGroup]).to(["read", "create", "update"]),
1137
+ ]),
1138
+ /** Who is told about review requests: scope default, mode or automation. Changed by data, not code. */
1139
+ ReviewRecipientRule: modelWithoutSubscriptions({
1140
+ id: a.id().required(),
1141
+ scope: a.string().required(),
1142
+ key: a.string(),
1143
+ roles: a.string().array(),
1144
+ emails: a.string().array(),
1145
+ updatedBy: a.string(),
1146
+ updatedAt: a.datetime().required(),
1147
+ })
1148
+ .authorization((allow) => [
1149
+ allow.group("editor").to(["read"]),
1150
+ allow.group(adminGroup).to(authoringOperations),
1151
+ ]),
1085
1152
  NewsroomSection: modelWithoutSubscriptions({
1086
1153
  id: a.id().required(),
1087
1154
  tenantId: a.string(),
@@ -2021,6 +2088,7 @@ function buildBaseDefinitions(includeEditions) {
2021
2088
  ]),
2022
2089
  ...editorialChecksSchemaDefinitions,
2023
2090
  ...monitoringViewSchemaDefinitions,
2091
+ ...automationViewSchemaDefinitions,
2024
2092
  };
2025
2093
  if (!includeEditions) {
2026
2094
  const mutableDefinitions = definitions;
@@ -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 { automationViewSchemaDefinitions } from "../functions/automation-view/schema";
5
6
  import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema";
6
7
  import { contentActions } from "../functions/content-actions/resource";
7
8
  import { knowledgeQuery } from "../functions/knowledge-query/resource";
@@ -1164,6 +1165,8 @@ const definitions = {
1164
1165
  error: a.json(),
1165
1166
  attempt: a.integer(),
1166
1167
  newsroomFeedKey: a.string(),
1168
+ usage: a.json(),
1169
+ costUsd: a.float(),
1167
1170
  })
1168
1171
  .secondaryIndexes((index) => [
1169
1172
  index("procedureId").sortKeys(["requestedAt"]).queryField("listProcedureRunsByProcedureAndRequestedAt"),
@@ -1176,6 +1179,74 @@ const definitions = {
1176
1179
  allow.group(adminGroup).to(authoringOperations),
1177
1180
  ]),
1178
1181
 
1182
+ /** A named "when X happens, run procedure Y" (PPY-9ff95b). Step 1 mirrors the monitoring router's procedureTriggers and starts nothing itself. */
1183
+ Automation: modelWithoutSubscriptions({
1184
+ id: a.id().required(),
1185
+ title: a.string().required(),
1186
+ description: a.string(),
1187
+ enabled: a.boolean().required(),
1188
+ trigger: a.json().required(),
1189
+ procedureKey: a.string().required(),
1190
+ inputTemplate: a.json(),
1191
+ limits: a.json(),
1192
+ createdBy: a.string(),
1193
+ createdAt: a.datetime().required(),
1194
+ updatedBy: a.string(),
1195
+ updatedAt: a.datetime().required(),
1196
+ })
1197
+ .authorization((allow) => [
1198
+ allow.group("editor").to(["read", "create", "update"]),
1199
+ allow.group(adminGroup).to(authoringOperations),
1200
+ ]),
1201
+
1202
+ /** A human review point raised by a step on any item (PPY-ab6a50). Review is not a story state. */
1203
+ ReviewRequest: modelWithoutSubscriptions({
1204
+ id: a.id().required(),
1205
+ subject: a.json().required(),
1206
+ mode: a.string().required(),
1207
+ reason: a.string().required(),
1208
+ ask: a.string(),
1209
+ raisedBy: a.string().required(),
1210
+ raisedAt: a.datetime().required(),
1211
+ deadline: a.datetime(),
1212
+ state: a.string().required(),
1213
+ answer: a.json(),
1214
+ links: a.json(),
1215
+ history: a.json(),
1216
+ appliedIn: a.json(),
1217
+ updatedAt: a.datetime().required(),
1218
+ })
1219
+ .authorization((allow) => [
1220
+ allow.group("editor").to(["read", "create", "update"]),
1221
+ allow.group(adminGroup).to(authoringOperations),
1222
+ ]),
1223
+
1224
+ /** One user's read mark on one review request; id is "<userKey>#<requestId>". */
1225
+ ReviewReadMark: modelWithoutSubscriptions({
1226
+ id: a.id().required(),
1227
+ requestId: a.string().required(),
1228
+ userKey: a.string().required(),
1229
+ readAt: a.datetime().required(),
1230
+ })
1231
+ .authorization((allow) => [
1232
+ allow.groups(["editor", adminGroup]).to(["read", "create", "update"]),
1233
+ ]),
1234
+
1235
+ /** Who is told about review requests: scope default, mode or automation. Changed by data, not code. */
1236
+ ReviewRecipientRule: modelWithoutSubscriptions({
1237
+ id: a.id().required(),
1238
+ scope: a.string().required(),
1239
+ key: a.string(),
1240
+ roles: a.string().array(),
1241
+ emails: a.string().array(),
1242
+ updatedBy: a.string(),
1243
+ updatedAt: a.datetime().required(),
1244
+ })
1245
+ .authorization((allow) => [
1246
+ allow.group("editor").to(["read"]),
1247
+ allow.group(adminGroup).to(authoringOperations),
1248
+ ]),
1249
+
1179
1250
  NewsroomSection: modelWithoutSubscriptions({
1180
1251
  id: a.id().required(),
1181
1252
  tenantId: a.string(),
@@ -2145,6 +2216,7 @@ const definitions = {
2145
2216
  ]),
2146
2217
  ...editorialChecksSchemaDefinitions,
2147
2218
  ...monitoringViewSchemaDefinitions,
2219
+ ...automationViewSchemaDefinitions,
2148
2220
  };
2149
2221
  if (!includeEditions) {
2150
2222
  const mutableDefinitions = definitions as Record<string, unknown>;
@@ -0,0 +1,17 @@
1
+ """The automation-view function: `automationOverview` (editors, admins and operator IAM roles, read only) and `runAutomation` (editors and admins)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from papyrus_content.automation_view import handle
8
+
9
+
10
+ def _account(context: Any) -> str | None:
11
+ arn = getattr(context, "invoked_function_arn", None)
12
+ parts = str(arn or "").split(":")
13
+ return parts[4] if len(parts) > 4 and parts[4] else None
14
+
15
+
16
+ def handler(event: dict[str, Any], context: Any = None) -> dict[str, Any]:
17
+ return handle(event, account_id=_account(context))
@@ -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 table read and ten run lookups need a few. */
6
+ export const AUTOMATION_VIEW_TIMEOUT_SECONDS = 25;
7
+ /** stdlib plus the runtime's boto3: the bundle is `papyrus_content` and nothing else. */
8
+ export const automationView = defineFunction((scope) => new Function(scope, "papyrus-automation-view", {
9
+ runtime: Runtime.PYTHON_3_12,
10
+ architecture: Architecture.ARM_64,
11
+ handler: "handler.handler",
12
+ timeout: Duration.seconds(AUTOMATION_VIEW_TIMEOUT_SECONDS),
13
+ memorySize: 256,
14
+ code: pythonLambdaCode({ functionDir: "automation-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/automation-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 table read and ten run lookups need a few. */
8
+ export const AUTOMATION_VIEW_TIMEOUT_SECONDS = 25;
9
+
10
+ /** stdlib plus the runtime's boto3: the bundle is `papyrus_content` and nothing else. */
11
+ export const automationView = defineFunction(
12
+ (scope: Construct) =>
13
+ new Function(scope, "papyrus-automation-view", {
14
+ runtime: Runtime.PYTHON_3_12,
15
+ architecture: Architecture.ARM_64,
16
+ handler: "handler.handler",
17
+ timeout: Duration.seconds(AUTOMATION_VIEW_TIMEOUT_SECONDS),
18
+ memorySize: 256,
19
+ code: pythonLambdaCode(
20
+ { functionDir: "automation-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/automation-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,25 @@
1
+ import { a } from "@aws-amplify/backend";
2
+ import { automationView } from "./resource.js";
3
+ const editors = ["editor", "admin"];
4
+ /**
5
+ * Core to every site's data API. The Automation page reads the automation records, the monitoring
6
+ * trigger rows they link to, and the procedure runs through `automationOverview`, and starts a run
7
+ * through `runAutomation`. Both admit editors and admins. AppSync also lets an IAM principal call the
8
+ * query when its own policy allows the field (every operation in this schema accepts IAM callers for
9
+ * the functions that call it); the function reads only for an IAM principal of its own account and refuses
10
+ * every IAM caller on `runAutomation`.
11
+ */
12
+ export const automationViewSchemaDefinitions = {
13
+ automationOverview: a
14
+ .query()
15
+ .arguments({ automationId: a.string() })
16
+ .returns(a.json())
17
+ .authorization((allow) => [allow.groups(editors)])
18
+ .handler(a.handler.function(automationView)),
19
+ runAutomation: a
20
+ .mutation()
21
+ .arguments({ automationId: a.string().required(), input: a.json() })
22
+ .returns(a.json())
23
+ .authorization((allow) => [allow.groups(editors)])
24
+ .handler(a.handler.function(automationView)),
25
+ };
@@ -0,0 +1,27 @@
1
+ import { a } from "@aws-amplify/backend";
2
+ import { automationView } from "./resource";
3
+
4
+ const editors = ["editor", "admin"];
5
+
6
+ /**
7
+ * Core to every site's data API. The Automation page reads the automation records, the monitoring
8
+ * trigger rows they link to, and the procedure runs through `automationOverview`, and starts a run
9
+ * through `runAutomation`. Both admit editors and admins. AppSync also lets an IAM principal call the
10
+ * query when its own policy allows the field (every operation in this schema accepts IAM callers for
11
+ * the functions that call it); the function reads only for an IAM principal of its own account and refuses
12
+ * every IAM caller on `runAutomation`.
13
+ */
14
+ export const automationViewSchemaDefinitions = {
15
+ automationOverview: a
16
+ .query()
17
+ .arguments({ automationId: a.string() })
18
+ .returns(a.json())
19
+ .authorization((allow) => [allow.groups(editors)])
20
+ .handler(a.handler.function(automationView)),
21
+ runAutomation: a
22
+ .mutation()
23
+ .arguments({ automationId: a.string().required(), input: a.json() })
24
+ .returns(a.json())
25
+ .authorization((allow) => [allow.groups(editors)])
26
+ .handler(a.handler.function(automationView)),
27
+ };
@@ -0,0 +1,56 @@
1
+ import { Stack } from "aws-cdk-lib";
2
+ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
3
+ import { MONITORING_TABLE_PARAMETER, MONITORING_VIEW_TABLE_ACTIONS, monitoringViewParameterArn, monitoringViewTableArns, } from "../monitoring-view/wire.js";
4
+ /** The reads the function makes of its own site's API with its own role. It writes nothing there. */
5
+ export const AUTOMATION_VIEW_FUNCTION_FIELDS = [
6
+ "Query/fields/getAutomation",
7
+ "Query/fields/getNewsroomProcedureDefinition",
8
+ "Query/fields/getProcedureRun",
9
+ "Query/fields/listAutomations",
10
+ "Query/fields/listProcedureRunsByProcedureAndRequestedAt",
11
+ ];
12
+ /** The procedure runner stack's function (infra/procedures/papyrus-procedure-runner.yaml); Run now starts it asynchronously. */
13
+ export const PROCEDURE_RUNNER_FUNCTION_NAME = "papyrus-procedure-runner";
14
+ /** Run now writes one row of its own to the monitoring table, in a monitor's partition (`MONITOR#<id>`), and nothing else. */
15
+ export const AUTOMATION_RUN_NOW_TABLE_ACTIONS = ["dynamodb:PutItem"];
16
+ export const AUTOMATION_RUN_NOW_LEADING_KEYS = ["MONITOR#*"];
17
+ /**
18
+ * Environment and least-privilege IAM for the automation-view function: a few read fields of the site's own API,
19
+ * the same read-only grant on the monitoring table that the Monitoring page has, one PutItem for Run now's row,
20
+ * and permission to start the procedure runner. A site outside the
21
+ * monitoring account finds no parameter and the page lists its records without runs.
22
+ */
23
+ export function wireAutomationView(backend) {
24
+ const lambda = backend.automationView.resources.lambda;
25
+ const stack = Stack.of(lambda);
26
+ lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
27
+ lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
28
+ lambda.addEnvironment("PROCEDURE_RUNNER_FUNCTION", PROCEDURE_RUNNER_FUNCTION_NAME);
29
+ const apiId = backend.data.resources.graphqlApi.apiId;
30
+ lambda.addToRolePolicy(new PolicyStatement({
31
+ effect: Effect.ALLOW,
32
+ actions: ["appsync:GraphQL"],
33
+ resources: AUTOMATION_VIEW_FUNCTION_FIELDS.map((field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${apiId}/types/${field}`),
34
+ }));
35
+ lambda.addToRolePolicy(new PolicyStatement({
36
+ effect: Effect.ALLOW,
37
+ actions: ["ssm:GetParameter"],
38
+ resources: [monitoringViewParameterArn(stack.region, stack.account)],
39
+ }));
40
+ lambda.addToRolePolicy(new PolicyStatement({
41
+ effect: Effect.ALLOW,
42
+ actions: MONITORING_VIEW_TABLE_ACTIONS,
43
+ resources: monitoringViewTableArns(stack.region, stack.account),
44
+ }));
45
+ lambda.addToRolePolicy(new PolicyStatement({
46
+ effect: Effect.ALLOW,
47
+ actions: AUTOMATION_RUN_NOW_TABLE_ACTIONS,
48
+ resources: monitoringViewTableArns(stack.region, stack.account).slice(0, 1),
49
+ conditions: { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": AUTOMATION_RUN_NOW_LEADING_KEYS } },
50
+ }));
51
+ lambda.addToRolePolicy(new PolicyStatement({
52
+ effect: Effect.ALLOW,
53
+ actions: ["lambda:InvokeFunction"],
54
+ resources: [`arn:aws:lambda:${stack.region}:${stack.account}:function:${PROCEDURE_RUNNER_FUNCTION_NAME}`],
55
+ }));
56
+ }
@@ -0,0 +1,76 @@
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
+ import {
5
+ MONITORING_TABLE_PARAMETER,
6
+ MONITORING_VIEW_TABLE_ACTIONS,
7
+ monitoringViewParameterArn,
8
+ monitoringViewTableArns,
9
+ } from "../monitoring-view/wire";
10
+
11
+ /** The reads the function makes of its own site's API with its own role. It writes nothing there. */
12
+ export const AUTOMATION_VIEW_FUNCTION_FIELDS = [
13
+ "Query/fields/getAutomation",
14
+ "Query/fields/getNewsroomProcedureDefinition",
15
+ "Query/fields/getProcedureRun",
16
+ "Query/fields/listAutomations",
17
+ "Query/fields/listProcedureRunsByProcedureAndRequestedAt",
18
+ ];
19
+
20
+ /** The procedure runner stack's function (infra/procedures/papyrus-procedure-runner.yaml); Run now starts it asynchronously. */
21
+ export const PROCEDURE_RUNNER_FUNCTION_NAME = "papyrus-procedure-runner";
22
+
23
+ /** Run now writes one row of its own to the monitoring table, in a monitor's partition (`MONITOR#<id>`), and nothing else. */
24
+ export const AUTOMATION_RUN_NOW_TABLE_ACTIONS = ["dynamodb:PutItem"];
25
+ export const AUTOMATION_RUN_NOW_LEADING_KEYS = ["MONITOR#*"];
26
+
27
+ /**
28
+ * Environment and least-privilege IAM for the automation-view function: a few read fields of the site's own API,
29
+ * the same read-only grant on the monitoring table that the Monitoring page has, one PutItem for Run now's row,
30
+ * and permission to start the procedure runner. A site outside the
31
+ * monitoring account finds no parameter and the page lists its records without runs.
32
+ */
33
+ export function wireAutomationView(backend: any): void {
34
+ const lambda = backend.automationView.resources.lambda as LambdaFunction;
35
+ const stack = Stack.of(lambda);
36
+ lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl as string);
37
+ lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
38
+ lambda.addEnvironment("PROCEDURE_RUNNER_FUNCTION", PROCEDURE_RUNNER_FUNCTION_NAME);
39
+ const apiId = backend.data.resources.graphqlApi.apiId as string;
40
+ lambda.addToRolePolicy(
41
+ new PolicyStatement({
42
+ effect: Effect.ALLOW,
43
+ actions: ["appsync:GraphQL"],
44
+ resources: AUTOMATION_VIEW_FUNCTION_FIELDS.map((field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${apiId}/types/${field}`),
45
+ }),
46
+ );
47
+ lambda.addToRolePolicy(
48
+ new PolicyStatement({
49
+ effect: Effect.ALLOW,
50
+ actions: ["ssm:GetParameter"],
51
+ resources: [monitoringViewParameterArn(stack.region, stack.account)],
52
+ }),
53
+ );
54
+ lambda.addToRolePolicy(
55
+ new PolicyStatement({
56
+ effect: Effect.ALLOW,
57
+ actions: MONITORING_VIEW_TABLE_ACTIONS,
58
+ resources: monitoringViewTableArns(stack.region, stack.account),
59
+ }),
60
+ );
61
+ lambda.addToRolePolicy(
62
+ new PolicyStatement({
63
+ effect: Effect.ALLOW,
64
+ actions: AUTOMATION_RUN_NOW_TABLE_ACTIONS,
65
+ resources: monitoringViewTableArns(stack.region, stack.account).slice(0, 1),
66
+ conditions: { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": AUTOMATION_RUN_NOW_LEADING_KEYS } },
67
+ }),
68
+ );
69
+ lambda.addToRolePolicy(
70
+ new PolicyStatement({
71
+ effect: Effect.ALLOW,
72
+ actions: ["lambda:InvokeFunction"],
73
+ resources: [`arn:aws:lambda:${stack.region}:${stack.account}:function:${PROCEDURE_RUNNER_FUNCTION_NAME}`],
74
+ }),
75
+ );
76
+ }
@@ -7,7 +7,7 @@ export const AI_MODEL_NAME = /^[a-z0-9][a-z0-9.-]{0,63}$/;
7
7
  export const SUBMISSION_KIND_NAME = /^[a-z][a-z0-9-]{0,23}$/;
8
8
  export const SUBJECT_TYPES = ["item", "submission", "story", "proposal", "concept"];
9
9
  export const COMPUTE_ROLE_ARN = /^arn:aws:iam::[0-9]{12}:role\/[A-Za-z0-9+=,.@_/-]{1,512}$/;
10
- const KNOWN_KEYS = new Set(["name", "computeRoleArn", "dimensions", "limits", "reservedHandles", "karma", "ranking", "snapshot", "retirementPoll", "reservedConcurrency", "hypeGap", "submissions", "comments", "moderation", "subjects"]);
10
+ const KNOWN_KEYS = new Set(["name", "computeRoleArn", "dimensions", "limits", "reservedHandles", "karma", "ranking", "snapshot", "retirementPoll", "reservedConcurrency", "hypeGap", "submissions", "comments", "moderation", "subjects", "proposals"]);
11
11
  function object(raw, label, known) {
12
12
  if (raw === undefined)
13
13
  return {};
@@ -117,6 +117,7 @@ export function resolveReaderSteeringConfig(request) {
117
117
  if (!Array.isArray(subjectTypes) || subjectTypes.length === 0 || !subjectTypes.every((type) => SUBJECT_TYPES.includes(type))) {
118
118
  throw new Error(`reader-steering plugin subjects.types must be a non-empty list of: ${SUBJECT_TYPES.join(", ")}`);
119
119
  }
120
+ const proposals = object(raw.proposals, "proposals", ["publish"]);
120
121
  const kinds = names(submissions.kinds, "submissions.kinds", SUBMISSION_KIND_NAME);
121
122
  const comments = object(raw.comments, "comments", ["perHour", "editWindowMinutes", "maximumLength", "maximumLinks"]);
122
123
  const hypeGap = resolveHypeGap(raw.hypeGap, dimensions);
@@ -173,6 +174,7 @@ export function resolveReaderSteeringConfig(request) {
173
174
  maximumLength: integer(comments.maximumLength, "comments.maximumLength", 2000, 1, 10000),
174
175
  maximumLinks: integer(comments.maximumLinks, "comments.maximumLinks", 3, 0, 50),
175
176
  },
177
+ proposals: { publish: flag(proposals.publish, "proposals.publish") },
176
178
  retirementPoll,
177
179
  ...(hypeGap ? { hypeGap } : {}),
178
180
  ...(raw.reservedConcurrency === undefined ? {} : { reservedConcurrency: integer(raw.reservedConcurrency, "reservedConcurrency", 1, 1, 1000) }),