@anthusai/papyrus 5.43.0-next.5 → 5.43.0-next.51

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 (144) hide show
  1. package/amplify/data/message-actions.js +68 -0
  2. package/amplify/data/message-actions.ts +70 -0
  3. package/amplify/data/resolvers/delete-own-message.js +30 -0
  4. package/amplify/data/resolvers/edit-message.js +33 -0
  5. package/amplify/data/resolvers/moderate-message.js +32 -0
  6. package/amplify/data/resolvers/post-message.js +68 -0
  7. package/amplify/data/resolvers/rename-thread.js +28 -0
  8. package/amplify/data/resource.js +34 -3
  9. package/amplify/data/resource.ts +35 -3
  10. package/amplify/data/schema.js +40 -5
  11. package/amplify/data/schema.ts +41 -5
  12. package/amplify/functions/category-action/handler.ts +53 -126
  13. package/amplify/functions/content-actions/handler.py +8 -0
  14. package/amplify/functions/content-actions/requirements.txt +2 -0
  15. package/amplify/functions/content-actions/resource.js +1 -1
  16. package/amplify/functions/content-actions/resource.ts +1 -1
  17. package/amplify/functions/model-attachment-upload/handler.ts +1 -0
  18. package/amplify/functions/monitoring-view/schema.js +16 -0
  19. package/amplify/functions/monitoring-view/schema.ts +16 -0
  20. package/amplify/functions/procedure-action/handler.ts +2 -10
  21. package/amplify/functions/proposal-actions/proposals.py +20 -3
  22. package/amplify/functions/shared/reference-curation-metadata.ts +21 -29
  23. package/amplify/plugins/reader-steering/function/handler.py +1 -3
  24. package/amplify/plugins/reader-steering/function/moderation.py +19 -0
  25. package/app/api/newsroom/source-frameable/route.ts +12 -0
  26. package/app/api/newsroom/source-icon/route.ts +20 -0
  27. package/app/newsroom/[section]/[...selection]/page.tsx +2 -0
  28. package/app/newsroom/[section]/page.tsx +1 -0
  29. package/bin/papyrus-app.mjs +12 -1
  30. package/bin/papyrus-infra.mjs +21 -1
  31. package/components/auto-accepted-badge.tsx +21 -0
  32. package/components/conversation-context.tsx +79 -0
  33. package/components/conversation-provider.tsx +24 -0
  34. package/components/conversation-thread.tsx +302 -0
  35. package/components/conversations-memory-source.ts +83 -0
  36. package/components/conversations-network-source.ts +52 -0
  37. package/components/conversations-page.tsx +14 -0
  38. package/components/conversations-source.ts +36 -0
  39. package/components/conversations-view.tsx +680 -0
  40. package/components/decision-control.tsx +307 -0
  41. package/components/dev-sandbox-editor-sign-in.tsx +2 -2
  42. package/components/list-detail-layout.tsx +44 -0
  43. package/components/markdown-context.tsx +412 -0
  44. package/components/monitoring-overview-view.tsx +143 -21
  45. package/components/monitoring-panel.tsx +64 -4
  46. package/components/needs-me-bodies.tsx +302 -0
  47. package/components/needs-me-detail.tsx +24 -0
  48. package/components/needs-me-feed.tsx +584 -0
  49. package/components/needs-me-reference-detail.tsx +79 -0
  50. package/components/needs-me-shared.tsx +48 -0
  51. package/components/news-desk-client-provider.tsx +24 -3
  52. package/components/news-desk-page.tsx +2 -1
  53. package/components/news-desk-taxonomy-client.ts +457 -81
  54. package/components/newsroom-access-gate.tsx +13 -20
  55. package/components/newsroom-article-editor.tsx +10 -4
  56. package/components/newsroom-board-skeleton.tsx +20 -0
  57. package/components/newsroom-board-view.tsx +2 -6
  58. package/components/newsroom-client-shell.tsx +2 -1
  59. package/components/newsroom-ops-overview.tsx +88 -73
  60. package/components/newsroom-ops-shell.tsx +8 -28
  61. package/components/newsroom-organizer.css +137 -4
  62. package/components/newsroom-references-view.tsx +328 -211
  63. package/components/notification-cards.tsx +370 -0
  64. package/components/notification-center.tsx +374 -213
  65. package/components/papyrus-console-shell.tsx +2 -1
  66. package/components/proposals-panel.tsx +21 -24
  67. package/components/proposals-view.tsx +8 -6
  68. package/components/reader-auth-control.tsx +3 -1
  69. package/components/reference-capture-client.ts +138 -0
  70. package/components/reference-capture-panel.tsx +268 -0
  71. package/components/reference-conversation.tsx +48 -0
  72. package/components/reference-feed.tsx +525 -0
  73. package/components/reference-review-card.tsx +62 -197
  74. package/components/reference-source-preview.tsx +27 -5
  75. package/components/reference-stories.tsx +34 -0
  76. package/components/references-page-loading.tsx +67 -0
  77. package/components/references-prefetch.ts +52 -0
  78. package/components/story-reporting-trail-client.ts +48 -10
  79. package/components/story-reporting-trail.tsx +32 -13
  80. package/components/topic-steering-workspace.tsx +94 -1225
  81. package/components/ui/auto-grow-textarea.tsx +18 -0
  82. package/components/use-conversation.ts +118 -0
  83. package/components/use-feed-preferences.ts +33 -0
  84. package/components/use-media-query.ts +16 -0
  85. package/components/use-reference-preview-attachments.ts +101 -0
  86. package/components/use-reference-ratings.ts +192 -0
  87. package/infra/amplify-app-shell.js +18 -2
  88. package/infra/amplify-app-shell.ts +18 -2
  89. package/infra/chat-stack.js +222 -0
  90. package/infra/chat-stack.ts +250 -0
  91. package/infra/chat.test.js +142 -0
  92. package/infra/chat.test.ts +151 -0
  93. package/infra/index.js +1 -0
  94. package/infra/site-config.js +58 -1
  95. package/infra/site-config.ts +78 -1
  96. package/lib/category-repository.ts +20 -39
  97. package/lib/console-chat-client.ts +8 -178
  98. package/lib/content-actions-client.ts +20 -0
  99. package/lib/conversation-feed.ts +96 -0
  100. package/lib/conversation.ts +225 -0
  101. package/lib/cyclotron-suggestions.ts +18 -36
  102. package/lib/local-preview-feed-fixture.ts +63 -0
  103. package/lib/local-preview-seed.ts +40 -29
  104. package/lib/local-preview-synthetic-rejects.ts +41 -0
  105. package/lib/monitoring-client.ts +29 -2
  106. package/lib/monitoring-overview.ts +52 -1
  107. package/lib/monitoring-paging.ts +58 -0
  108. package/lib/needs-me-items.ts +289 -0
  109. package/lib/needs-me-ranking.ts +302 -0
  110. package/lib/newsroom-all-pages.ts +21 -0
  111. package/lib/newsroom-board.ts +10 -1
  112. package/lib/newsroom-chat-identity.ts +2 -1
  113. package/lib/newsroom-demo-dashboard.ts +0 -7
  114. package/lib/newsroom-index-filters.ts +18 -0
  115. package/lib/newsroom-nav.ts +1 -1
  116. package/lib/newsroom-organizer-nav.ts +2 -1
  117. package/lib/newsroom-references.ts +25 -1
  118. package/lib/notification-feed.ts +212 -0
  119. package/lib/post-message.ts +54 -0
  120. package/lib/proposals-admin.ts +2 -2
  121. package/lib/proposals-client.ts +29 -0
  122. package/lib/reader-subject.ts +3 -2
  123. package/lib/reference-capture.ts +294 -0
  124. package/lib/reference-feed-cache.ts +170 -0
  125. package/lib/reference-feed-keys.ts +69 -0
  126. package/lib/reference-feed-ranking.ts +153 -0
  127. package/lib/reference-policy.ts +95 -11
  128. package/lib/reference-preview.ts +112 -0
  129. package/lib/reference-review-details.ts +21 -57
  130. package/lib/relevance-decisions.ts +6 -2
  131. package/lib/review-client.ts +71 -5
  132. package/lib/review-drafts.ts +277 -0
  133. package/lib/review-fixture.ts +57 -3
  134. package/lib/review-requests.ts +89 -24
  135. package/lib/semantic-graph.ts +1 -1
  136. package/lib/site-brand.ts +2 -0
  137. package/lib/source-frameable.ts +77 -0
  138. package/lib/source-icon.ts +104 -0
  139. package/lib/staff-groups.ts +23 -0
  140. package/lib/staging-gate.ts +2 -2
  141. package/lib/story-reporting-trail.ts +61 -4
  142. package/package.json +55 -2
  143. package/routes.manifest.json +23 -1
  144. package/components/newsroom-overview-skeleton.tsx +0 -25
@@ -0,0 +1,68 @@
1
+ import { a } from "@aws-amplify/backend";
2
+ /**
3
+ * Messages belong to their author, and the backend says so. The browser cannot create, edit or delete a Message through the
4
+ * model (editors and admins only read it); it calls these four mutations, whose resolvers stamp the author from the signed-in
5
+ * identity and check ownership against the stored `authorSub` in the same DynamoDB write. `renameThread` does the same for a
6
+ * conversation's title. Functions and IAM callers (the router, procedures, the console responder, email, Slack) write the
7
+ * model directly with their own grants, as before.
8
+ *
9
+ * Each resolver is one self-contained file (AppSync takes a single file and offers `@aws-appsync/utils` only).
10
+ */
11
+ const people = (allow) => [allow.group("editor"), allow.group("admin")];
12
+ const adminsOnly = (allow) => [allow.group("admin")];
13
+ const onMessages = (entry) => a.handler.custom({ dataSource: a.ref("Message"), entry });
14
+ const onThreads = (entry) => a.handler.custom({ dataSource: a.ref("MessageThread"), entry });
15
+ export const messageActionsSchemaDefinitions = {
16
+ postMessage: a
17
+ .mutation()
18
+ .arguments({
19
+ id: a.id().required(),
20
+ threadId: a.id().required(),
21
+ content: a.string(),
22
+ summary: a.string(),
23
+ parentMessageId: a.id(),
24
+ sequenceNumber: a.integer(),
25
+ messageKind: a.string().required(),
26
+ messageDomain: a.string().required(),
27
+ messageType: a.string(),
28
+ role: a.string(),
29
+ source: a.string(),
30
+ authorLabel: a.string(),
31
+ semanticLayer: a.string(),
32
+ searchVisibility: a.string(),
33
+ responseTarget: a.string(),
34
+ responseStatus: a.string(),
35
+ responseOwner: a.string(),
36
+ responseStartedAt: a.datetime(),
37
+ responseCompletedAt: a.datetime(),
38
+ metadata: a.json(),
39
+ newsroomFeedKey: a.string(),
40
+ })
41
+ .returns(a.ref("Message"))
42
+ .authorization(people)
43
+ .handler(onMessages("./resolvers/post-message.js")),
44
+ editMessage: a
45
+ .mutation()
46
+ .arguments({ id: a.id().required(), content: a.string().required(), summary: a.string() })
47
+ .returns(a.ref("Message"))
48
+ .authorization(people)
49
+ .handler(onMessages("./resolvers/edit-message.js")),
50
+ deleteOwnMessage: a
51
+ .mutation()
52
+ .arguments({ id: a.id().required() })
53
+ .returns(a.ref("Message"))
54
+ .authorization(people)
55
+ .handler(onMessages("./resolvers/delete-own-message.js")),
56
+ moderateMessage: a
57
+ .mutation()
58
+ .arguments({ id: a.id().required() })
59
+ .returns(a.ref("Message"))
60
+ .authorization(adminsOnly)
61
+ .handler(onMessages("./resolvers/moderate-message.js")),
62
+ renameThread: a
63
+ .mutation()
64
+ .arguments({ id: a.id().required(), title: a.string().required() })
65
+ .returns(a.ref("MessageThread"))
66
+ .authorization(people)
67
+ .handler(onThreads("./resolvers/rename-thread.js")),
68
+ };
@@ -0,0 +1,70 @@
1
+ import { a } from "@aws-amplify/backend";
2
+
3
+ /**
4
+ * Messages belong to their author, and the backend says so. The browser cannot create, edit or delete a Message through the
5
+ * model (editors and admins only read it); it calls these four mutations, whose resolvers stamp the author from the signed-in
6
+ * identity and check ownership against the stored `authorSub` in the same DynamoDB write. `renameThread` does the same for a
7
+ * conversation's title. Functions and IAM callers (the router, procedures, the console responder, email, Slack) write the
8
+ * model directly with their own grants, as before.
9
+ *
10
+ * Each resolver is one self-contained file (AppSync takes a single file and offers `@aws-appsync/utils` only).
11
+ */
12
+ const people = (allow: any) => [allow.group("editor"), allow.group("admin")];
13
+ const adminsOnly = (allow: any) => [allow.group("admin")];
14
+ const onMessages = (entry: string) => a.handler.custom({ dataSource: a.ref("Message"), entry });
15
+ const onThreads = (entry: string) => a.handler.custom({ dataSource: a.ref("MessageThread"), entry });
16
+
17
+ export const messageActionsSchemaDefinitions = {
18
+ postMessage: a
19
+ .mutation()
20
+ .arguments({
21
+ id: a.id().required(),
22
+ threadId: a.id().required(),
23
+ content: a.string(),
24
+ summary: a.string(),
25
+ parentMessageId: a.id(),
26
+ sequenceNumber: a.integer(),
27
+ messageKind: a.string().required(),
28
+ messageDomain: a.string().required(),
29
+ messageType: a.string(),
30
+ role: a.string(),
31
+ source: a.string(),
32
+ authorLabel: a.string(),
33
+ semanticLayer: a.string(),
34
+ searchVisibility: a.string(),
35
+ responseTarget: a.string(),
36
+ responseStatus: a.string(),
37
+ responseOwner: a.string(),
38
+ responseStartedAt: a.datetime(),
39
+ responseCompletedAt: a.datetime(),
40
+ metadata: a.json(),
41
+ newsroomFeedKey: a.string(),
42
+ })
43
+ .returns(a.ref("Message"))
44
+ .authorization(people)
45
+ .handler(onMessages("./resolvers/post-message.js")),
46
+ editMessage: a
47
+ .mutation()
48
+ .arguments({ id: a.id().required(), content: a.string().required(), summary: a.string() })
49
+ .returns(a.ref("Message"))
50
+ .authorization(people)
51
+ .handler(onMessages("./resolvers/edit-message.js")),
52
+ deleteOwnMessage: a
53
+ .mutation()
54
+ .arguments({ id: a.id().required() })
55
+ .returns(a.ref("Message"))
56
+ .authorization(people)
57
+ .handler(onMessages("./resolvers/delete-own-message.js")),
58
+ moderateMessage: a
59
+ .mutation()
60
+ .arguments({ id: a.id().required() })
61
+ .returns(a.ref("Message"))
62
+ .authorization(adminsOnly)
63
+ .handler(onMessages("./resolvers/moderate-message.js")),
64
+ renameThread: a
65
+ .mutation()
66
+ .arguments({ id: a.id().required(), title: a.string().required() })
67
+ .returns(a.ref("MessageThread"))
68
+ .authorization(people)
69
+ .handler(onThreads("./resolvers/rename-thread.js")),
70
+ };
@@ -0,0 +1,30 @@
1
+ // deleteOwnMessage: the author deletes their own Message. A soft delete: the Message stays (replies keep their place) marked
2
+ // deleted, with when and by whom. Nothing is removed.
3
+ import { util } from "@aws-appsync/utils";
4
+ import * as ddb from "@aws-appsync/utils/dynamodb";
5
+
6
+ export function request(ctx) {
7
+ const identity = ctx.identity;
8
+ if (!identity || !identity.sub) util.error("Sign in to delete a message.", "Unauthorized");
9
+ const now = util.time.nowISO8601();
10
+ return ddb.update({
11
+ key: { id: ctx.args.id },
12
+ update: {
13
+ status: ddb.operations.replace("deleted"),
14
+ deletedAt: ddb.operations.replace(now),
15
+ deletedBySub: ddb.operations.replace(identity.sub),
16
+ updatedAt: ddb.operations.replace(now),
17
+ },
18
+ condition: { authorSub: { eq: identity.sub }, status: { ne: "deleted" } },
19
+ });
20
+ }
21
+
22
+ export function response(ctx) {
23
+ if (ctx.error) {
24
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
25
+ util.error("Only the author can delete their own message, and it may already be deleted.", "Unauthorized");
26
+ }
27
+ util.error(ctx.error.message, ctx.error.type);
28
+ }
29
+ return ctx.result;
30
+ }
@@ -0,0 +1,33 @@
1
+ // editMessage: a person changes the text of their own Message, and no one else's. The condition is checked by DynamoDB against the
2
+ // stored authorSub in the same write, so there is no window between the check and the change. A deleted Message cannot be edited.
3
+ // Authorship cannot be reassigned: the update never touches authorSub.
4
+ import { util } from "@aws-appsync/utils";
5
+ import * as ddb from "@aws-appsync/utils/dynamodb";
6
+
7
+ export function request(ctx) {
8
+ const identity = ctx.identity;
9
+ if (!identity || !identity.sub) util.error("Sign in to edit a message.", "Unauthorized");
10
+ const content = typeof ctx.args.content === "string" ? ctx.args.content.trim() : "";
11
+ if (!content) util.error("A message cannot be empty.", "BadRequest");
12
+ const now = util.time.nowISO8601();
13
+ return ddb.update({
14
+ key: { id: ctx.args.id },
15
+ update: {
16
+ content: ddb.operations.replace(content),
17
+ summary: ddb.operations.replace(ctx.args.summary ? ctx.args.summary : content.slice(0, 120)),
18
+ editedAt: ddb.operations.replace(now),
19
+ updatedAt: ddb.operations.replace(now),
20
+ },
21
+ condition: { authorSub: { eq: identity.sub }, status: { ne: "deleted" } },
22
+ });
23
+ }
24
+
25
+ export function response(ctx) {
26
+ if (ctx.error) {
27
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
28
+ util.error("Only the author can edit a message, and a deleted message cannot be edited.", "Unauthorized");
29
+ }
30
+ util.error(ctx.error.message, ctx.error.type);
31
+ }
32
+ return ctx.result;
33
+ }
@@ -0,0 +1,32 @@
1
+ // moderateMessage: an admin removes anyone's Message. The same soft delete as the author's, with no change to the text, and only
2
+ // for the admin group (checked here as well as by the mutation's authorization).
3
+ import { util } from "@aws-appsync/utils";
4
+ import * as ddb from "@aws-appsync/utils/dynamodb";
5
+
6
+ export function request(ctx) {
7
+ const identity = ctx.identity;
8
+ if (!identity || !identity.sub) util.error("Sign in to moderate a message.", "Unauthorized");
9
+ const groups = identity.groups || [];
10
+ if (groups.indexOf("admin") < 0) util.error("Only an admin can delete someone else's message.", "Unauthorized");
11
+ const now = util.time.nowISO8601();
12
+ return ddb.update({
13
+ key: { id: ctx.args.id },
14
+ update: {
15
+ status: ddb.operations.replace("deleted"),
16
+ deletedAt: ddb.operations.replace(now),
17
+ deletedBySub: ddb.operations.replace(identity.sub),
18
+ updatedAt: ddb.operations.replace(now),
19
+ },
20
+ condition: { id: { attributeExists: true }, status: { ne: "deleted" } },
21
+ });
22
+ }
23
+
24
+ export function response(ctx) {
25
+ if (ctx.error) {
26
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
27
+ util.error("That message does not exist or is already deleted.", "NotFound");
28
+ }
29
+ util.error(ctx.error.message, ctx.error.type);
30
+ }
31
+ return ctx.result;
32
+ }
@@ -0,0 +1,68 @@
1
+ // postMessage: the only way a person's Message is created. The author is the signed-in user (the identity's sub), whatever the
2
+ // client sends: the mutation has no author argument, and a label is only a label. One DynamoDB PutItem that refuses to replace an
3
+ // existing Message (ids are derived from the sequence number, so a lost race comes back as a conflict the caller retries).
4
+ import { util } from "@aws-appsync/utils";
5
+ import * as ddb from "@aws-appsync/utils/dynamodb";
6
+
7
+ function clean(value) {
8
+ const out = {};
9
+ const keys = Object.keys(value);
10
+ for (let index = 0; index < keys.length; index++) {
11
+ const key = keys[index];
12
+ if (value[key] !== null && value[key] !== undefined) out[key] = value[key];
13
+ }
14
+ return out;
15
+ }
16
+
17
+ export function request(ctx) {
18
+ const identity = ctx.identity;
19
+ if (!identity || !identity.sub) util.error("Sign in to post a message.", "Unauthorized");
20
+ const args = ctx.args;
21
+ const content = typeof args.content === "string" ? args.content.trim() : "";
22
+ // A placeholder waiting for a responder (PENDING) may start empty; any other message needs text.
23
+ if (!content && args.responseStatus !== "PENDING") util.error("A message cannot be empty.", "BadRequest");
24
+ if (!args.id || !args.threadId) util.error("A message needs an id and a conversation.", "BadRequest");
25
+ const now = util.time.nowISO8601();
26
+ const claims = identity.claims || {};
27
+ const email = typeof claims.email === "string" ? claims.email : "";
28
+ const label = typeof args.authorLabel === "string" && args.authorLabel.trim() ? args.authorLabel.trim() : email ? email : "editor";
29
+ const item = clean({
30
+ __typename: "Message",
31
+ id: args.id,
32
+ messageKind: args.messageKind,
33
+ messageDomain: args.messageDomain,
34
+ status: "active",
35
+ summary: args.summary ? args.summary : content.slice(0, 120),
36
+ source: args.source,
37
+ authorSub: identity.sub,
38
+ authorLabel: label,
39
+ threadId: args.threadId,
40
+ parentMessageId: args.parentMessageId,
41
+ sequenceNumber: args.sequenceNumber,
42
+ role: args.role ? args.role : "human",
43
+ messageType: args.messageType,
44
+ content: content,
45
+ semanticLayer: args.semanticLayer,
46
+ searchVisibility: args.searchVisibility,
47
+ responseTarget: args.responseTarget,
48
+ responseStatus: args.responseStatus ? args.responseStatus : "COMPLETED",
49
+ responseOwner: args.responseOwner,
50
+ responseStartedAt: args.responseStartedAt,
51
+ responseCompletedAt: args.responseCompletedAt,
52
+ metadata: args.metadata,
53
+ createdAt: now,
54
+ updatedAt: now,
55
+ newsroomFeedKey: args.newsroomFeedKey,
56
+ });
57
+ return ddb.put({ key: { id: args.id }, item: item, condition: { id: { attributeExists: false } } });
58
+ }
59
+
60
+ export function response(ctx) {
61
+ if (ctx.error) {
62
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
63
+ util.error("A message with this id already exists (conditional check failed); try again.", "Conflict");
64
+ }
65
+ util.error(ctx.error.message, ctx.error.type);
66
+ }
67
+ return ctx.result;
68
+ }
@@ -0,0 +1,28 @@
1
+ // renameThread: the person who started a conversation, or an admin, gives it a new title. The browser's own update of a thread
2
+ // cannot change the title (the field is read and create only), so this is the one way. Nothing else about the thread changes.
3
+ import { util } from "@aws-appsync/utils";
4
+ import * as ddb from "@aws-appsync/utils/dynamodb";
5
+
6
+ export function request(ctx) {
7
+ const identity = ctx.identity;
8
+ if (!identity || !identity.sub) util.error("Sign in to rename a conversation.", "Unauthorized");
9
+ const title = typeof ctx.args.title === "string" ? ctx.args.title.trim() : "";
10
+ if (!title) util.error("A title cannot be empty.", "BadRequest");
11
+ const groups = identity.groups || [];
12
+ const isAdmin = groups.indexOf("admin") >= 0;
13
+ return ddb.update({
14
+ key: { id: ctx.args.id },
15
+ update: { title: ddb.operations.replace(title.slice(0, 120)) },
16
+ condition: isAdmin ? { id: { attributeExists: true } } : { createdBySub: { eq: identity.sub } },
17
+ });
18
+ }
19
+
20
+ export function response(ctx) {
21
+ if (ctx.error) {
22
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
23
+ util.error("Only the person who started this conversation, or an admin, can rename it.", "Unauthorized");
24
+ }
25
+ util.error(ctx.error.message, ctx.error.type);
26
+ }
27
+ return ctx.result;
28
+ }
@@ -247,8 +247,6 @@ const schema = a.schema({
247
247
  referenceId: a.id().required(),
248
248
  status: a.string().required(),
249
249
  reasonCode: a.string(),
250
- messageId: a.id(),
251
- relationId: a.id(),
252
250
  }),
253
251
  ReferenceQualityActionResult: a.customType({
254
252
  ok: a.boolean().required(),
@@ -603,6 +601,12 @@ const schema = a.schema({
603
601
  .returns(a.json())
604
602
  .authorization((allow) => [allow.groups(contentWriteGroups)])
605
603
  .handler(functionHandler("contentActions", contentActions)),
604
+ correctReferenceCapture: a
605
+ .mutation()
606
+ .arguments({ input: a.json().required() })
607
+ .returns(a.json())
608
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
609
+ .handler(functionHandler("contentActions", contentActions)),
606
610
  saveItemDraft: a
607
611
  .mutation()
608
612
  .arguments({ input: a.json().required() })
@@ -1563,8 +1567,26 @@ const schema = a.schema({
1563
1567
  curationStatusUpdatedAt: a.datetime(),
1564
1568
  curationStatusUpdatedBy: a.string(),
1565
1569
  curationStatusReason: a.string(),
1570
+ curationReasonCode: a.string(),
1571
+ curationActorKind: a.string(),
1572
+ curationSectionId: a.string(),
1573
+ curationShareable: a.boolean(),
1574
+ curationDelegation: a.json(),
1575
+ curationDecisionRelationId: a.string(),
1566
1576
  newsroomFeedKey: a.string(),
1567
1577
  reviewedFeedKey: a.string(),
1578
+ discoveredByMonitorId: a.string(),
1579
+ discoveryDetails: a.json(),
1580
+ discoveryRule: a.json(),
1581
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1582
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1583
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1584
+ // P(accept), written once, and never rewritten by a vote.
1585
+ feedStateKey: a.string(),
1586
+ feedArrivalAt: a.datetime(),
1587
+ predictedAcceptProbability: a.float(),
1588
+ predictionDecidedAt: a.datetime(),
1589
+ predictionSortKey: a.string(),
1568
1590
  updatedAt: a.datetime(),
1569
1591
  })
1570
1592
  .secondaryIndexes((index) => [
@@ -1577,6 +1599,9 @@ const schema = a.schema({
1577
1599
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1578
1600
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1579
1601
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1602
+ index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
1603
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1604
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1580
1605
  ])
1581
1606
  .authorization((allow) => [
1582
1607
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1664,6 +1689,9 @@ const schema = a.schema({
1664
1689
  authorSub: a.string(),
1665
1690
  authorUserProfileId: a.id(),
1666
1691
  authorLabel: a.string(),
1692
+ editedAt: a.datetime(),
1693
+ deletedAt: a.datetime(),
1694
+ deletedBySub: a.string(),
1667
1695
  threadId: a.id(),
1668
1696
  parentMessageId: a.id(),
1669
1697
  sequenceNumber: a.integer(),
@@ -1698,7 +1726,10 @@ const schema = a.schema({
1698
1726
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1699
1727
  ])
1700
1728
  .authorization((allow) => [
1701
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1729
+ // Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
1730
+ // postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
1731
+ allow.group("editor").to(["read"]),
1732
+ allow.group(adminGroup).to(["read"]),
1702
1733
  allow.authenticated("identityPool").to(["read", "create", "update"]),
1703
1734
  ]),
1704
1735
  MessageThread: a
@@ -267,8 +267,6 @@ const schema = a.schema({
267
267
  referenceId: a.id().required(),
268
268
  status: a.string().required(),
269
269
  reasonCode: a.string(),
270
- messageId: a.id(),
271
- relationId: a.id(),
272
270
  }),
273
271
 
274
272
  ReferenceQualityActionResult: a.customType({
@@ -657,6 +655,13 @@ const schema = a.schema({
657
655
  .authorization((allow) => [allow.groups(contentWriteGroups)])
658
656
  .handler(functionHandler("contentActions", contentActions)),
659
657
 
658
+ correctReferenceCapture: a
659
+ .mutation()
660
+ .arguments({ input: a.json().required() })
661
+ .returns(a.json())
662
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
663
+ .handler(functionHandler("contentActions", contentActions)),
664
+
660
665
  saveItemDraft: a
661
666
  .mutation()
662
667
  .arguments({ input: a.json().required() })
@@ -1675,8 +1680,26 @@ const schema = a.schema({
1675
1680
  curationStatusUpdatedAt: a.datetime(),
1676
1681
  curationStatusUpdatedBy: a.string(),
1677
1682
  curationStatusReason: a.string(),
1683
+ curationReasonCode: a.string(),
1684
+ curationActorKind: a.string(),
1685
+ curationSectionId: a.string(),
1686
+ curationShareable: a.boolean(),
1687
+ curationDelegation: a.json(),
1688
+ curationDecisionRelationId: a.string(),
1678
1689
  newsroomFeedKey: a.string(),
1679
1690
  reviewedFeedKey: a.string(),
1691
+ discoveredByMonitorId: a.string(),
1692
+ discoveryDetails: a.json(),
1693
+ discoveryRule: a.json(),
1694
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1695
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1696
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1697
+ // P(accept), written once, and never rewritten by a vote.
1698
+ feedStateKey: a.string(),
1699
+ feedArrivalAt: a.datetime(),
1700
+ predictedAcceptProbability: a.float(),
1701
+ predictionDecidedAt: a.datetime(),
1702
+ predictionSortKey: a.string(),
1680
1703
  updatedAt: a.datetime(),
1681
1704
  })
1682
1705
  .secondaryIndexes((index) => [
@@ -1689,6 +1712,9 @@ const schema = a.schema({
1689
1712
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1690
1713
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1691
1714
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1715
+ index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
1716
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1717
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1692
1718
  ])
1693
1719
  .authorization((allow) => [
1694
1720
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1779,6 +1805,9 @@ const schema = a.schema({
1779
1805
  authorSub: a.string(),
1780
1806
  authorUserProfileId: a.id(),
1781
1807
  authorLabel: a.string(),
1808
+ editedAt: a.datetime(),
1809
+ deletedAt: a.datetime(),
1810
+ deletedBySub: a.string(),
1782
1811
  threadId: a.id(),
1783
1812
  parentMessageId: a.id(),
1784
1813
  sequenceNumber: a.integer(),
@@ -1813,7 +1842,10 @@ const schema = a.schema({
1813
1842
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1814
1843
  ])
1815
1844
  .authorization((allow) => [
1816
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1845
+ // Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
1846
+ // postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
1847
+ allow.group("editor").to(["read"]),
1848
+ allow.group(adminGroup).to(["read"]),
1817
1849
  allow.authenticated("identityPool").to(["read", "create", "update"]),
1818
1850
  ]),
1819
1851
 
@@ -4,6 +4,7 @@ import { categoryAction } from "../functions/category-action/resource.js";
4
4
  import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema.js";
5
5
  import { automationViewSchemaDefinitions } from "../functions/automation-view/schema.js";
6
6
  import { proposalActionsSchemaDefinitions } from "../functions/proposal-actions/schema.js";
7
+ import { messageActionsSchemaDefinitions } from "./message-actions.js";
7
8
  import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema.js";
8
9
  import { contentActions } from "../functions/content-actions/resource.js";
9
10
  import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
@@ -259,8 +260,6 @@ function buildBaseDefinitions(includeEditions) {
259
260
  referenceId: a.id().required(),
260
261
  status: a.string().required(),
261
262
  reasonCode: a.string(),
262
- messageId: a.id(),
263
- relationId: a.id(),
264
263
  }),
265
264
  ReferenceQualityActionResult: a.customType({
266
265
  ok: a.boolean().required(),
@@ -530,6 +529,12 @@ function buildBaseDefinitions(includeEditions) {
530
529
  .returns(a.json())
531
530
  .authorization((allow) => [allow.groups(contentWriteGroups)])
532
531
  .handler(functionHandler("contentActions", contentActions)),
532
+ correctReferenceCapture: a
533
+ .mutation()
534
+ .arguments({ input: a.json().required() })
535
+ .returns(a.json())
536
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
537
+ .handler(functionHandler("contentActions", contentActions)),
533
538
  saveItemDraft: a
534
539
  .mutation()
535
540
  .arguments({ input: a.json().required() })
@@ -1469,8 +1474,26 @@ function buildBaseDefinitions(includeEditions) {
1469
1474
  curationStatusUpdatedAt: a.datetime(),
1470
1475
  curationStatusUpdatedBy: a.string(),
1471
1476
  curationStatusReason: a.string(),
1477
+ curationReasonCode: a.string(),
1478
+ curationActorKind: a.string(),
1479
+ curationSectionId: a.string(),
1480
+ curationShareable: a.boolean(),
1481
+ curationDelegation: a.json(),
1482
+ curationDecisionRelationId: a.string(),
1472
1483
  newsroomFeedKey: a.string(),
1473
1484
  reviewedFeedKey: a.string(),
1485
+ discoveredByMonitorId: a.string(),
1486
+ discoveryDetails: a.json(),
1487
+ discoveryRule: a.json(),
1488
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1489
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1490
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1491
+ // P(accept), written once, and never rewritten by a vote.
1492
+ feedStateKey: a.string(),
1493
+ feedArrivalAt: a.datetime(),
1494
+ predictedAcceptProbability: a.float(),
1495
+ predictionDecidedAt: a.datetime(),
1496
+ predictionSortKey: a.string(),
1474
1497
  updatedAt: a.datetime(),
1475
1498
  })
1476
1499
  .secondaryIndexes((index) => [
@@ -1483,6 +1506,9 @@ function buildBaseDefinitions(includeEditions) {
1483
1506
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1484
1507
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1485
1508
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1509
+ index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
1510
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1511
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1486
1512
  ])
1487
1513
  .authorization((allow) => [
1488
1514
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1567,6 +1593,9 @@ function buildBaseDefinitions(includeEditions) {
1567
1593
  authorSub: a.string(),
1568
1594
  authorUserProfileId: a.id(),
1569
1595
  authorLabel: a.string(),
1596
+ editedAt: a.datetime(),
1597
+ deletedAt: a.datetime(),
1598
+ deletedBySub: a.string(),
1570
1599
  threadId: a.id(),
1571
1600
  parentMessageId: a.id(),
1572
1601
  sequenceNumber: a.integer(),
@@ -1601,19 +1630,24 @@ function buildBaseDefinitions(includeEditions) {
1601
1630
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1602
1631
  ])
1603
1632
  .authorization((allow) => [
1604
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1633
+ // Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
1634
+ // postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
1635
+ allow.group("editor").to(["read"]),
1636
+ allow.group(adminGroup).to(["read"]),
1605
1637
  ]),
1606
1638
  MessageThread: realtimeModel({
1607
1639
  id: a.id().required(),
1608
1640
  threadKind: a.string().required(),
1609
1641
  status: a.string().required(),
1610
- title: a.string().required(),
1642
+ // The title and the creator are set when a conversation is created and read afterwards; a title changes only through
1643
+ // renameThread (its creator or an admin), and the creator never changes.
1644
+ title: a.string().required().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
1611
1645
  summary: a.string(),
1612
1646
  primaryAnchorKind: a.string(),
1613
1647
  primaryAnchorId: a.id(),
1614
1648
  primaryAnchorLineageId: a.id(),
1615
1649
  primaryAnchorKey: a.string(),
1616
- createdBySub: a.string(),
1650
+ createdBySub: a.string().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
1617
1651
  createdByUserProfileId: a.id(),
1618
1652
  createdByLabel: a.string(),
1619
1653
  messageCount: a.integer(),
@@ -2098,6 +2132,7 @@ function buildBaseDefinitions(includeEditions) {
2098
2132
  ...monitoringViewSchemaDefinitions,
2099
2133
  ...automationViewSchemaDefinitions,
2100
2134
  ...proposalActionsSchemaDefinitions,
2135
+ ...messageActionsSchemaDefinitions,
2101
2136
  };
2102
2137
  if (!includeEditions) {
2103
2138
  const mutableDefinitions = definitions;