@anthusai/papyrus 5.41.0 → 5.41.2
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/functions/content-actions/handler.py +2 -2
- package/amplify/functions/monitoring-view/wire.js +6 -4
- package/amplify/functions/monitoring-view/wire.ts +6 -4
- package/amplify/site-backend-config.js +19 -0
- package/amplify/site-backend-config.ts +27 -0
- package/amplify/site-backend.js +5 -3
- package/amplify/site-backend.ts +5 -3
- package/components/papyrus-private-chat-panel.tsx +85 -15
- package/lib/newsroom-chat-client.ts +30 -0
- package/lib/newsroom-chat-identity.ts +39 -0
- package/lib/papyrus-hosted-chat.ts +19 -6
- package/lib/papyrus-private-chat-client.ts +5 -4
- package/lib/prospectus-session.ts +2 -0
- package/lib/prospectus-sign-in-routes.ts +19 -2
- package/lib/prospectus-site-identity.ts +7 -0
- package/package.json +3 -1
- package/routes.manifest.json +1 -1
|
@@ -359,8 +359,8 @@ def handler(event, context):
|
|
|
359
359
|
if field == "intakeEmailSubmissionReferences":
|
|
360
360
|
client, _claims = create_authoring_client()
|
|
361
361
|
try:
|
|
362
|
-
from papyrus_content.monitoring_view import
|
|
363
|
-
receipt_store =
|
|
362
|
+
from papyrus_content.monitoring_view import default_email_store_factory
|
|
363
|
+
receipt_store = default_email_store_factory(os.environ)()
|
|
364
364
|
return intake_email_submission_references(
|
|
365
365
|
client, arguments, event.get("identity") or {}, receipt_store=receipt_store)
|
|
366
366
|
except IntakeError as error:
|
|
@@ -19,11 +19,12 @@ export function monitoringViewParameterArn(region, account) {
|
|
|
19
19
|
* Environment and least-privilege IAM for the monitoring-view function. A site outside the monitoring
|
|
20
20
|
* account simply finds no parameter and the page says monitoring is not configured.
|
|
21
21
|
*/
|
|
22
|
-
export function wireMonitoringView(backend) {
|
|
22
|
+
export function wireMonitoringView(backend, emailMonitorId = "hn") {
|
|
23
23
|
const lambda = backend.monitoringView.resources.lambda;
|
|
24
24
|
const stack = Stack.of(lambda);
|
|
25
25
|
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
|
|
26
26
|
lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
|
|
27
|
+
lambda.addEnvironment("PAPYRUS_EMAIL_MONITOR_ID", emailMonitorId);
|
|
27
28
|
lambda.addToRolePolicy(new PolicyStatement({
|
|
28
29
|
effect: Effect.ALLOW,
|
|
29
30
|
actions: ["ssm:GetParameter"],
|
|
@@ -41,13 +42,14 @@ export function wireMonitoringView(backend) {
|
|
|
41
42
|
effect: Effect.ALLOW,
|
|
42
43
|
actions: ["dynamodb:PutItem"],
|
|
43
44
|
resources: monitoringViewTableArns(stack.region, stack.account).slice(0, 1),
|
|
44
|
-
conditions: { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [
|
|
45
|
+
conditions: { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [`MONITOR#${emailMonitorId}`] } },
|
|
45
46
|
}));
|
|
46
47
|
}
|
|
47
48
|
/** Lets the authenticated Reference-intake action verify a selected email receipt. */
|
|
48
|
-
export function wireEmailReferenceIntake(lambda) {
|
|
49
|
+
export function wireEmailReferenceIntake(lambda, emailMonitorId = "hn") {
|
|
49
50
|
const stack = Stack.of(lambda);
|
|
50
51
|
lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
|
|
52
|
+
lambda.addEnvironment("PAPYRUS_EMAIL_MONITOR_ID", emailMonitorId);
|
|
51
53
|
lambda.addToRolePolicy(new PolicyStatement({
|
|
52
54
|
effect: Effect.ALLOW,
|
|
53
55
|
actions: ["ssm:GetParameter"],
|
|
@@ -57,6 +59,6 @@ export function wireEmailReferenceIntake(lambda) {
|
|
|
57
59
|
effect: Effect.ALLOW,
|
|
58
60
|
actions: MONITORING_VIEW_TABLE_ACTIONS,
|
|
59
61
|
resources: monitoringViewTableArns(stack.region, stack.account).slice(0, 1),
|
|
60
|
-
conditions: { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [
|
|
62
|
+
conditions: { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [`MONITOR#${emailMonitorId}`] } },
|
|
61
63
|
}));
|
|
62
64
|
}
|
|
@@ -25,11 +25,12 @@ export function monitoringViewParameterArn(region: string, account: string): str
|
|
|
25
25
|
* Environment and least-privilege IAM for the monitoring-view function. A site outside the monitoring
|
|
26
26
|
* account simply finds no parameter and the page says monitoring is not configured.
|
|
27
27
|
*/
|
|
28
|
-
export function wireMonitoringView(backend: any): void {
|
|
28
|
+
export function wireMonitoringView(backend: any, emailMonitorId = "hn"): void {
|
|
29
29
|
const lambda = backend.monitoringView.resources.lambda as LambdaFunction;
|
|
30
30
|
const stack = Stack.of(lambda);
|
|
31
31
|
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl as string);
|
|
32
32
|
lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
|
|
33
|
+
lambda.addEnvironment("PAPYRUS_EMAIL_MONITOR_ID", emailMonitorId);
|
|
33
34
|
lambda.addToRolePolicy(
|
|
34
35
|
new PolicyStatement({
|
|
35
36
|
effect: Effect.ALLOW,
|
|
@@ -52,15 +53,16 @@ export function wireMonitoringView(backend: any): void {
|
|
|
52
53
|
effect: Effect.ALLOW,
|
|
53
54
|
actions: ["dynamodb:PutItem"],
|
|
54
55
|
resources: monitoringViewTableArns(stack.region, stack.account).slice(0, 1),
|
|
55
|
-
conditions: { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [
|
|
56
|
+
conditions: { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [`MONITOR#${emailMonitorId}`] } },
|
|
56
57
|
}),
|
|
57
58
|
);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/** Lets the authenticated Reference-intake action verify a selected email receipt. */
|
|
61
|
-
export function wireEmailReferenceIntake(lambda: LambdaFunction): void {
|
|
62
|
+
export function wireEmailReferenceIntake(lambda: LambdaFunction, emailMonitorId = "hn"): void {
|
|
62
63
|
const stack = Stack.of(lambda);
|
|
63
64
|
lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
|
|
65
|
+
lambda.addEnvironment("PAPYRUS_EMAIL_MONITOR_ID", emailMonitorId);
|
|
64
66
|
lambda.addToRolePolicy(
|
|
65
67
|
new PolicyStatement({
|
|
66
68
|
effect: Effect.ALLOW,
|
|
@@ -73,7 +75,7 @@ export function wireEmailReferenceIntake(lambda: LambdaFunction): void {
|
|
|
73
75
|
effect: Effect.ALLOW,
|
|
74
76
|
actions: MONITORING_VIEW_TABLE_ACTIONS,
|
|
75
77
|
resources: monitoringViewTableArns(stack.region, stack.account).slice(0, 1),
|
|
76
|
-
conditions: { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [
|
|
78
|
+
conditions: { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [`MONITOR#${emailMonitorId}`] } },
|
|
77
79
|
}),
|
|
78
80
|
);
|
|
79
81
|
}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
export const MAXIMUM_HTML_S_MAXAGE_SECONDS = 86400;
|
|
2
|
+
const MONITOR_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
3
|
+
export function validateMonitoringConfig(monitoring) {
|
|
4
|
+
if (monitoring === undefined)
|
|
5
|
+
return [];
|
|
6
|
+
if (typeof monitoring !== "object" || monitoring === null || Array.isArray(monitoring)) {
|
|
7
|
+
return ["monitoring must be an object"];
|
|
8
|
+
}
|
|
9
|
+
const config = monitoring;
|
|
10
|
+
if (config.emailMonitorId !== undefined
|
|
11
|
+
&& (typeof config.emailMonitorId !== "string" || !MONITOR_ID_PATTERN.test(config.emailMonitorId))) {
|
|
12
|
+
return ["monitoring.emailMonitorId must be a lowercase monitor id (1-63 letters, digits, or hyphens)"];
|
|
13
|
+
}
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
export function assertValidMonitoringConfig(monitoring) {
|
|
17
|
+
const problems = validateMonitoringConfig(monitoring);
|
|
18
|
+
if (problems.length > 0)
|
|
19
|
+
throw new Error(`Invalid backend.monitoring: ${problems.join("; ")}`);
|
|
20
|
+
}
|
|
2
21
|
const CLOUDFRONT_DISTRIBUTION_ARN = /^arn:aws[a-z-]*:cloudfront::\d{12}:distribution\/[A-Z0-9]+$/;
|
|
3
22
|
function isRelativePathWithoutParentSegments(value) {
|
|
4
23
|
if (value === "" || value.startsWith("/") || value.includes("\\"))
|
|
@@ -40,6 +40,31 @@ export type PapyrusPublicationCacheConfig = {
|
|
|
40
40
|
|
|
41
41
|
export const MAXIMUM_HTML_S_MAXAGE_SECONDS = 86400;
|
|
42
42
|
|
|
43
|
+
export type PapyrusMonitoringConfig = {
|
|
44
|
+
/** Existing shared Monitoring partition that stores this site's private email receipts. */
|
|
45
|
+
emailMonitorId?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const MONITOR_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
49
|
+
|
|
50
|
+
export function validateMonitoringConfig(monitoring: unknown): string[] {
|
|
51
|
+
if (monitoring === undefined) return [];
|
|
52
|
+
if (typeof monitoring !== "object" || monitoring === null || Array.isArray(monitoring)) {
|
|
53
|
+
return ["monitoring must be an object"];
|
|
54
|
+
}
|
|
55
|
+
const config = monitoring as { emailMonitorId?: unknown };
|
|
56
|
+
if (config.emailMonitorId !== undefined
|
|
57
|
+
&& (typeof config.emailMonitorId !== "string" || !MONITOR_ID_PATTERN.test(config.emailMonitorId))) {
|
|
58
|
+
return ["monitoring.emailMonitorId must be a lowercase monitor id (1-63 letters, digits, or hyphens)"];
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function assertValidMonitoringConfig(monitoring: unknown): void {
|
|
64
|
+
const problems = validateMonitoringConfig(monitoring);
|
|
65
|
+
if (problems.length > 0) throw new Error(`Invalid backend.monitoring: ${problems.join("; ")}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
export type PapyrusPublicationConfig = {
|
|
44
69
|
kind: "s3-projection";
|
|
45
70
|
/** Site repo path (relative, `.py`) of the SiteProjector bundled into content-actions as `site_projector`. */
|
|
@@ -138,6 +163,8 @@ export type PapyrusSiteBackendConfig = {
|
|
|
138
163
|
*/
|
|
139
164
|
productionAppId?: string;
|
|
140
165
|
auth?: PapyrusAuthConfig;
|
|
166
|
+
/** Shared Monitoring partition used by private email intake for this publication. */
|
|
167
|
+
monitoring?: PapyrusMonitoringConfig;
|
|
141
168
|
/**
|
|
142
169
|
* Maximum number of data model nested stacks CloudFormation creates at once.
|
|
143
170
|
* Stacks are chained in waves of this size so a new AWS account is not hit
|
package/amplify/site-backend.js
CHANGED
|
@@ -14,7 +14,7 @@ import { planRenditions } from "./publication/renditions-settings.js";
|
|
|
14
14
|
import { addProjectionLeaseAccess } from "./publication/projection-lease.js";
|
|
15
15
|
import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings.js";
|
|
16
16
|
import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
|
|
17
|
-
import { assertValidPublicationConfig } from "./site-backend-config.js";
|
|
17
|
+
import { assertValidMonitoringConfig, assertValidPublicationConfig } from "./site-backend-config.js";
|
|
18
18
|
import { revalidateSecretReadStatement } from "./revalidate-secret-policy.js";
|
|
19
19
|
import { revalidateDistributionEnvironment, revalidateInvalidationStatement, validateRevalidateDistributionIds, } from "./revalidate-invalidation-settings.js";
|
|
20
20
|
import { addInvalidationPathAlarm } from "./publication/invalidation-alarm.js";
|
|
@@ -87,6 +87,8 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
87
87
|
const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
|
|
88
88
|
if (publication !== undefined)
|
|
89
89
|
assertValidPublicationConfig(publication);
|
|
90
|
+
assertValidMonitoringConfig(site.monitoring);
|
|
91
|
+
const emailMonitorId = site.monitoring?.emailMonitorId ?? "hn";
|
|
90
92
|
const publicationPlan = planS3ProjectionBackend(publication);
|
|
91
93
|
const renditionsPlan = planRenditions(publication, process.cwd());
|
|
92
94
|
setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
|
|
@@ -336,7 +338,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
336
338
|
// and publication to this deployment's pool and existing API endpoint.
|
|
337
339
|
contentActionsLambda.addEnvironment("PAPYRUS_USER_POOL_ISSUER", `https://cognito-idp.${Stack.of(contentActionsLambda).region}.amazonaws.com/${backend.auth.resources.userPool.userPoolId}`);
|
|
338
340
|
contentActionsLambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
|
|
339
|
-
wireEmailReferenceIntake(contentActionsLambda);
|
|
341
|
+
wireEmailReferenceIntake(contentActionsLambda, emailMonitorId);
|
|
340
342
|
contentActionsLambda.addToRolePolicy(new PolicyStatement({
|
|
341
343
|
actions: ["appsync:GraphQL"],
|
|
342
344
|
resources: ["*"],
|
|
@@ -627,7 +629,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
627
629
|
},
|
|
628
630
|
});
|
|
629
631
|
wireEditorialChecks(backend);
|
|
630
|
-
wireMonitoringView(backend);
|
|
632
|
+
wireMonitoringView(backend, emailMonitorId);
|
|
631
633
|
wireAutomationView(backend);
|
|
632
634
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
633
635
|
return backend;
|
package/amplify/site-backend.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { planRenditions } from "./publication/renditions-settings";
|
|
|
17
17
|
import { addProjectionLeaseAccess } from "./publication/projection-lease";
|
|
18
18
|
import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings";
|
|
19
19
|
import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
|
|
20
|
-
import { assertValidPublicationConfig } from "./site-backend-config";
|
|
20
|
+
import { assertValidMonitoringConfig, assertValidPublicationConfig } from "./site-backend-config";
|
|
21
21
|
import { revalidateSecretReadStatement } from "./revalidate-secret-policy";
|
|
22
22
|
import {
|
|
23
23
|
revalidateDistributionEnvironment,
|
|
@@ -108,6 +108,8 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
108
108
|
};
|
|
109
109
|
const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
|
|
110
110
|
if (publication !== undefined) assertValidPublicationConfig(publication);
|
|
111
|
+
assertValidMonitoringConfig(site.monitoring);
|
|
112
|
+
const emailMonitorId = site.monitoring?.emailMonitorId ?? "hn";
|
|
111
113
|
const publicationPlan = planS3ProjectionBackend(publication);
|
|
112
114
|
const renditionsPlan = planRenditions(publication, process.cwd());
|
|
113
115
|
setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
|
|
@@ -429,7 +431,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
429
431
|
"PAPYRUS_GRAPHQL_ENDPOINT",
|
|
430
432
|
backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
|
|
431
433
|
);
|
|
432
|
-
wireEmailReferenceIntake(contentActionsLambda);
|
|
434
|
+
wireEmailReferenceIntake(contentActionsLambda, emailMonitorId);
|
|
433
435
|
contentActionsLambda.addToRolePolicy(
|
|
434
436
|
new PolicyStatement({
|
|
435
437
|
actions: ["appsync:GraphQL"],
|
|
@@ -773,7 +775,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
773
775
|
},
|
|
774
776
|
});
|
|
775
777
|
wireEditorialChecks(backend);
|
|
776
|
-
wireMonitoringView(backend);
|
|
778
|
+
wireMonitoringView(backend, emailMonitorId);
|
|
777
779
|
wireAutomationView(backend);
|
|
778
780
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
779
781
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { connectNewsroomChat } from "../lib/newsroom-chat-client";
|
|
5
|
+
import { loadProspectusAccountState, selectProspectusAccount, type ProspectusAccountState } from "../lib/prospectus-account-client";
|
|
4
6
|
import { PapyrusConsoleConversation, type ConsoleConversationMessage } from "./papyrus-console-conversation";
|
|
5
7
|
import {
|
|
6
8
|
listPapyrusPrivateChatMessages,
|
|
@@ -28,9 +30,13 @@ function failureMessage(error: unknown): string {
|
|
|
28
30
|
export function PapyrusPrivateChatPanel({ onClose }: { onClose: () => void }) {
|
|
29
31
|
const [channel, setChannel] = useState<PapyrusPrivateChatChannel | null>(null);
|
|
30
32
|
const [messages, setMessages] = useState<PapyrusPrivateChatMessage[]>([]);
|
|
33
|
+
const [accountState, setAccountState] = useState<ProspectusAccountState | null>(null);
|
|
34
|
+
const [connecting, setConnecting] = useState(false);
|
|
35
|
+
const [connectionNotice, setConnectionNotice] = useState<string | null>(null);
|
|
31
36
|
const [loading, setLoading] = useState(true);
|
|
32
37
|
const [sending, setSending] = useState(false);
|
|
33
38
|
const [error, setError] = useState<string | null>(null);
|
|
39
|
+
const identityGeneration = useRef(0);
|
|
34
40
|
const retryAttempt = useRef<{ text: string; key: string } | null>(null);
|
|
35
41
|
const isWaitingForReply = messages.length > 0 && messages[messages.length - 1].author_kind === "human" &&
|
|
36
42
|
messages[messages.length - 1].addressed_to_bot_id !== null;
|
|
@@ -46,38 +52,66 @@ export function PapyrusPrivateChatPanel({ onClose }: { onClose: () => void }) {
|
|
|
46
52
|
}];
|
|
47
53
|
}, [isWaitingForReply, messages]);
|
|
48
54
|
|
|
49
|
-
const
|
|
55
|
+
const handleFailure = useCallback(async (nextError: unknown) => {
|
|
56
|
+
const failure = failureMessage(nextError);
|
|
57
|
+
setError(failure);
|
|
58
|
+
if (["sign_in_required", "chat_identity_connection_required", "publishing_account_required", "chat_account_access_denied"].includes(failure)) {
|
|
59
|
+
const generation = ++identityGeneration.current;
|
|
60
|
+
setChannel(null);
|
|
61
|
+
setMessages([]);
|
|
62
|
+
setAccountState(null);
|
|
63
|
+
retryAttempt.current = null;
|
|
64
|
+
setSending(false);
|
|
65
|
+
setLoading(false);
|
|
66
|
+
if (failure === "publishing_account_required") {
|
|
67
|
+
const state = await loadProspectusAccountState(window.location.pathname);
|
|
68
|
+
if (generation === identityGeneration.current) setAccountState(state);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const refreshMessages = useCallback(async (channelId: string, generation: number) => {
|
|
50
74
|
const next = await listPapyrusPrivateChatMessages(channelId);
|
|
51
|
-
setMessages(next);
|
|
75
|
+
if (generation === identityGeneration.current) setMessages(next);
|
|
52
76
|
}, []);
|
|
53
77
|
|
|
54
78
|
const openConversation = useCallback(async () => {
|
|
79
|
+
const generation = ++identityGeneration.current;
|
|
55
80
|
setLoading(true);
|
|
56
81
|
setError(null);
|
|
82
|
+
setChannel(null);
|
|
83
|
+
setMessages([]);
|
|
57
84
|
try {
|
|
58
85
|
const nextChannel = await openPapyrusPrivateChat();
|
|
86
|
+
if (generation !== identityGeneration.current) return;
|
|
59
87
|
setChannel(nextChannel);
|
|
60
|
-
await refreshMessages(nextChannel.channel_id);
|
|
88
|
+
await refreshMessages(nextChannel.channel_id, generation);
|
|
61
89
|
} catch (nextError) {
|
|
62
|
-
|
|
90
|
+
if (generation === identityGeneration.current) await handleFailure(nextError);
|
|
63
91
|
} finally {
|
|
64
|
-
setLoading(false);
|
|
92
|
+
if (generation === identityGeneration.current) setLoading(false);
|
|
65
93
|
}
|
|
66
|
-
}, [refreshMessages]);
|
|
94
|
+
}, [handleFailure, refreshMessages]);
|
|
67
95
|
|
|
68
96
|
useEffect(() => {
|
|
97
|
+
const code = new URLSearchParams(window.location.search).get("sign_in_error");
|
|
98
|
+
if (code) setConnectionNotice(code === "newsroom_identity_mismatch"
|
|
99
|
+
? "The selected Google account did not match your newsroom session. Connect again with the same account."
|
|
100
|
+
: "The publishing-account connection did not complete. You can safely try again.");
|
|
69
101
|
void openConversation();
|
|
102
|
+
return () => { identityGeneration.current += 1; };
|
|
70
103
|
}, [openConversation]);
|
|
71
104
|
|
|
72
105
|
useEffect(() => {
|
|
73
106
|
if (!channel || !isWaitingForReply) return;
|
|
74
107
|
let active = true;
|
|
108
|
+
const generation = identityGeneration.current;
|
|
75
109
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
76
110
|
const poll = async () => {
|
|
77
111
|
try {
|
|
78
|
-
await refreshMessages(channel.channel_id);
|
|
112
|
+
await refreshMessages(channel.channel_id, generation);
|
|
79
113
|
} catch (nextError) {
|
|
80
|
-
if (active)
|
|
114
|
+
if (active && generation === identityGeneration.current) await handleFailure(nextError);
|
|
81
115
|
return;
|
|
82
116
|
}
|
|
83
117
|
if (active) timer = setTimeout(poll, REFRESH_MS);
|
|
@@ -87,10 +121,11 @@ export function PapyrusPrivateChatPanel({ onClose }: { onClose: () => void }) {
|
|
|
87
121
|
active = false;
|
|
88
122
|
if (timer) clearTimeout(timer);
|
|
89
123
|
};
|
|
90
|
-
}, [channel, isWaitingForReply, refreshMessages]);
|
|
124
|
+
}, [channel, handleFailure, isWaitingForReply, refreshMessages]);
|
|
91
125
|
|
|
92
126
|
const sendMessage = useCallback(async (text: string) => {
|
|
93
127
|
if (!channel || !text.trim() || sending) return;
|
|
128
|
+
const generation = identityGeneration.current;
|
|
94
129
|
const attempt = retryAttempt.current?.text === text
|
|
95
130
|
? retryAttempt.current
|
|
96
131
|
: { text, key: globalThis.crypto.randomUUID() };
|
|
@@ -99,16 +134,40 @@ export function PapyrusPrivateChatPanel({ onClose }: { onClose: () => void }) {
|
|
|
99
134
|
setError(null);
|
|
100
135
|
try {
|
|
101
136
|
const result = await sendPapyrusPrivateChatMessage(channel.channel_id, text, attempt.key);
|
|
137
|
+
if (generation !== identityGeneration.current) return;
|
|
102
138
|
setMessages((current) => current.some((message) => message.message_id === result.message.message_id)
|
|
103
139
|
? current
|
|
104
140
|
: [...current, result.message]);
|
|
105
141
|
retryAttempt.current = null;
|
|
106
142
|
} catch (nextError) {
|
|
107
|
-
|
|
143
|
+
if (generation === identityGeneration.current) await handleFailure(nextError);
|
|
108
144
|
} finally {
|
|
109
|
-
setSending(false);
|
|
145
|
+
if (generation === identityGeneration.current) setSending(false);
|
|
146
|
+
}
|
|
147
|
+
}, [channel, handleFailure, sending]);
|
|
148
|
+
|
|
149
|
+
const connect = async () => {
|
|
150
|
+
setConnecting(true);
|
|
151
|
+
try {
|
|
152
|
+
const returnUrl = new URL(window.location.href);
|
|
153
|
+
returnUrl.searchParams.delete("sign_in_error");
|
|
154
|
+
const target = await connectNewsroomChat(`${returnUrl.pathname}${returnUrl.search}${returnUrl.hash}`);
|
|
155
|
+
window.location.assign(target);
|
|
156
|
+
} catch (nextError) {
|
|
157
|
+
setError(failureMessage(nextError));
|
|
158
|
+
setConnecting(false);
|
|
110
159
|
}
|
|
111
|
-
}
|
|
160
|
+
};
|
|
161
|
+
const chooseAccount = async (accountId: string) => {
|
|
162
|
+
setConnecting(true);
|
|
163
|
+
try {
|
|
164
|
+
const result = await selectProspectusAccount(accountId);
|
|
165
|
+
if ("error" in result) { setError(result.error); return; }
|
|
166
|
+
setAccountState(null);
|
|
167
|
+
await openConversation();
|
|
168
|
+
} catch (nextError) { setError(failureMessage(nextError)); }
|
|
169
|
+
finally { setConnecting(false); }
|
|
170
|
+
};
|
|
112
171
|
|
|
113
172
|
return <div className="papyrus-console__panel">
|
|
114
173
|
<header className="papyrus-console__header">
|
|
@@ -117,9 +176,20 @@ export function PapyrusPrivateChatPanel({ onClose }: { onClose: () => void }) {
|
|
|
117
176
|
</header>
|
|
118
177
|
{channel ? <p className="papyrus-console__empty" role="status">Private conversation for your selected Account.</p> : null}
|
|
119
178
|
{!channel && !loading ? <div className="papyrus-console__empty" role="alert">
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
179
|
+
{error === "chat_identity_connection_required" ? <>
|
|
180
|
+
<strong>Connect your publishing account</strong>
|
|
181
|
+
{connectionNotice ? <span>{connectionNotice}</span> : null}
|
|
182
|
+
<span>Continue with the same Google account you use in the newsroom, then choose the Account for this private conversation.</span>
|
|
183
|
+
<button className="papyrus-console__new-session" disabled={connecting} onClick={() => void connect()} type="button">{connecting ? "Connecting…" : "Connect publishing account"}</button>
|
|
184
|
+
</> : accountState?.status === "signedIn" ? <>
|
|
185
|
+
<strong>Choose your publishing Account</strong>
|
|
186
|
+
{accountState.publishingAccounts.length === 0 ? <span>This identity has no publishing Account for this site. Ask the Account owner to confirm your existing access.</span> : accountState.publishingAccounts.map(account => <button className="papyrus-console__new-session" disabled={connecting} key={account.accountId} onClick={() => void chooseAccount(account.accountId)} type="button">{account.name ?? account.accountId}</button>)}
|
|
187
|
+
{error !== "publishing_account_required" ? <span>{error}</span> : null}
|
|
188
|
+
</> : <>
|
|
189
|
+
<strong>Private chat could not be opened.</strong>
|
|
190
|
+
<span>{error === "sign_in_required" ? "Your newsroom session has expired. Sign in from the account menu, then retry." : error}</span>
|
|
191
|
+
<button className="papyrus-console__new-session" onClick={() => void openConversation()} type="button">Retry</button>
|
|
192
|
+
</>}
|
|
123
193
|
</div> : <PapyrusConsoleConversation
|
|
124
194
|
messages={conversationMessages}
|
|
125
195
|
isSending={sending || loading || !channel}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type NewsroomChatFetch = typeof fetch;
|
|
2
|
+
|
|
3
|
+
export function createNewsroomChatFetch(
|
|
4
|
+
accessToken: () => Promise<string | null>,
|
|
5
|
+
fetcher: NewsroomChatFetch,
|
|
6
|
+
): NewsroomChatFetch {
|
|
7
|
+
return async (input, init) => {
|
|
8
|
+
if (typeof input !== "string" || (!input.startsWith("/api/newsroom/chat/") && !input.startsWith("/api/auth/prospectus/"))) {
|
|
9
|
+
throw new Error("Newsroom credentials can only be sent to this site's chat identity routes.");
|
|
10
|
+
}
|
|
11
|
+
const token = await accessToken();
|
|
12
|
+
if (!token) throw new Error("sign_in_required");
|
|
13
|
+
const headers = new Headers(init?.headers);
|
|
14
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
15
|
+
return fetcher(input, { ...init, headers, credentials: "same-origin", cache: "no-store" });
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const newsroomChatFetch: NewsroomChatFetch = createNewsroomChatFetch(async () => {
|
|
20
|
+
const { fetchAuthSession } = await import("aws-amplify/auth");
|
|
21
|
+
const session = await fetchAuthSession();
|
|
22
|
+
return session.tokens?.accessToken?.toString() ?? null;
|
|
23
|
+
}, (input, init) => globalThis.fetch(input, init));
|
|
24
|
+
|
|
25
|
+
export async function connectNewsroomChat(returnPath: string, fetcher: NewsroomChatFetch = newsroomChatFetch): Promise<string> {
|
|
26
|
+
const response = await fetcher(`/api/auth/prospectus/sign-in?newsroomChat=true&returnTo=${encodeURIComponent(returnPath)}`, { method: "POST" });
|
|
27
|
+
const body = await response.json() as { authorizationUrl?: string; message?: string };
|
|
28
|
+
if (!response.ok || !body.authorizationUrl) throw new Error(body.message ?? "Private chat could not connect to your publishing account.");
|
|
29
|
+
return body.authorizationUrl;
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CognitoJwtVerifier } from "aws-jwt-verify";
|
|
2
|
+
import type { JwksCache } from "aws-jwt-verify/jwk";
|
|
3
|
+
import type { NextRequest } from "next/server";
|
|
4
|
+
import { readAmplifyOutputsFile } from "./amplify-outputs-path";
|
|
5
|
+
|
|
6
|
+
export type NewsroomChatIdentity = { subject: string };
|
|
7
|
+
export type NewsroomChatIdentityResolver = (request: NextRequest) => Promise<NewsroomChatIdentity | null>;
|
|
8
|
+
|
|
9
|
+
export function newsroomChatIdentityResolver(
|
|
10
|
+
auth: { user_pool_id: string; user_pool_client_id: string },
|
|
11
|
+
jwksCache?: JwksCache,
|
|
12
|
+
): NewsroomChatIdentityResolver {
|
|
13
|
+
const verifier = CognitoJwtVerifier.create({
|
|
14
|
+
userPoolId: auth.user_pool_id,
|
|
15
|
+
clientId: auth.user_pool_client_id,
|
|
16
|
+
tokenUse: "access",
|
|
17
|
+
groups: ["editor", "admin"],
|
|
18
|
+
}, jwksCache ? { jwksCache } : undefined);
|
|
19
|
+
return async (request) => {
|
|
20
|
+
const token = request.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/i)?.[1];
|
|
21
|
+
if (!token) return null;
|
|
22
|
+
try {
|
|
23
|
+
const claims = await verifier.verify(token);
|
|
24
|
+
return { subject: claims.sub };
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let configuredResolver: { key: string; resolve: NewsroomChatIdentityResolver } | undefined;
|
|
32
|
+
|
|
33
|
+
export async function resolveNewsroomChatIdentity(request: NextRequest): Promise<NewsroomChatIdentity | null> {
|
|
34
|
+
const auth = readAmplifyOutputsFile()?.auth as { user_pool_id?: string; user_pool_client_id?: string } | undefined;
|
|
35
|
+
if (!auth?.user_pool_id || !auth.user_pool_client_id) throw new Error("Newsroom identity configuration is unavailable");
|
|
36
|
+
const key = `${auth.user_pool_id}:${auth.user_pool_client_id}`;
|
|
37
|
+
if (configuredResolver?.key !== key) configuredResolver = { key, resolve: newsroomChatIdentityResolver({ user_pool_id: auth.user_pool_id, user_pool_client_id: auth.user_pool_client_id }) };
|
|
38
|
+
return configuredResolver.resolve(request);
|
|
39
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveNewsroomChatIdentity, type NewsroomChatIdentityResolver } from "./newsroom-chat-identity";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import type { EmbeddedChatAppDependencies } from "chatticus/chat-server";
|
|
3
4
|
import type { NextRequest } from "next/server";
|
|
@@ -24,6 +25,7 @@ export interface PapyrusChatScope {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface PapyrusHostedChatOptions {
|
|
28
|
+
resolveNewsroomIdentity?: NewsroomChatIdentityResolver;
|
|
27
29
|
resolveRuntime?: (siteOrigin: string) => Promise<ProspectusSignInRuntime | null>;
|
|
28
30
|
resolvePrivateDependencies?: () => Promise<EmbeddedChatAppDependencies | null>;
|
|
29
31
|
privateChatEnabled?: boolean;
|
|
@@ -61,7 +63,7 @@ async function principalForRequest(
|
|
|
61
63
|
request: NextRequest,
|
|
62
64
|
operation: "console:read" | "console:write",
|
|
63
65
|
options: PapyrusHostedChatOptions,
|
|
64
|
-
): Promise<{ principal: ResolvedPapyrusPrincipal | null; status: number }> {
|
|
66
|
+
): Promise<{ principal: ResolvedPapyrusPrincipal | null; status: number; error?: string }> {
|
|
65
67
|
const environment = options.environment ?? process.env;
|
|
66
68
|
const siteProduct = options.siteProduct !== undefined ? options.siteProduct : prospectusSiteProduct(environment);
|
|
67
69
|
if (!siteProduct) return { principal: null, status: 503 };
|
|
@@ -74,11 +76,22 @@ async function principalForRequest(
|
|
|
74
76
|
}
|
|
75
77
|
if (!runtime) return { principal: null, status: 503 };
|
|
76
78
|
|
|
79
|
+
let newsroomIdentity;
|
|
80
|
+
try {
|
|
81
|
+
newsroomIdentity = await (options.resolveNewsroomIdentity ?? resolveNewsroomChatIdentity)(request);
|
|
82
|
+
} catch {
|
|
83
|
+
return { principal: null, status: 503 };
|
|
84
|
+
}
|
|
85
|
+
if (!newsroomIdentity) return { principal: null, status: 401 };
|
|
77
86
|
const nowInSeconds = options.nowInSeconds?.() ?? Math.floor(Date.now() / 1000);
|
|
78
87
|
const session = signedInProspectusSession(request, runtime.sealingSecret, nowInSeconds);
|
|
79
|
-
if (!session || !
|
|
80
|
-
return { principal: null, status:
|
|
88
|
+
if (!session || !Number.isFinite(session.accessTokenExpiresAt) || session.accessTokenExpiresAt <= nowInSeconds) {
|
|
89
|
+
return { principal: null, status: 409, error: "chat_identity_connection_required" };
|
|
90
|
+
}
|
|
91
|
+
if (session.newsroomSubject !== newsroomIdentity.subject) {
|
|
92
|
+
return { principal: null, status: 409, error: "chat_identity_connection_required" };
|
|
81
93
|
}
|
|
94
|
+
if (!session.selectedAccountId) return { principal: null, status: 409, error: "publishing_account_required" };
|
|
82
95
|
|
|
83
96
|
try {
|
|
84
97
|
const directory = await runtime.accounts.accountDirectory(session.accessToken);
|
|
@@ -162,6 +175,9 @@ export async function handlePapyrusHostedChatRequest(
|
|
|
162
175
|
const privateChatEnabled = options.privateChatEnabled ?? environment.PAPYRUS_PRIVATE_CHAT_ENABLED === "true";
|
|
163
176
|
if (!privateChatEnabled) return jsonFailure(404, "private_chat_disabled");
|
|
164
177
|
|
|
178
|
+
const authorization = await principalForRequest(request, request.method === "POST" ? "console:write" : "console:read", options);
|
|
179
|
+
if (!authorization.principal) return jsonFailure(authorization.status, authorization.error ?? (authorization.status === 401 ? "sign_in_required" : authorization.status === 403 ? "chat_account_access_denied" : "chat_runtime_unavailable"));
|
|
180
|
+
|
|
165
181
|
let dependencies: EmbeddedChatAppDependencies | null;
|
|
166
182
|
try {
|
|
167
183
|
if (options.resolvePrivateDependencies) {
|
|
@@ -178,9 +194,6 @@ export async function handlePapyrusHostedChatRequest(
|
|
|
178
194
|
}
|
|
179
195
|
if (!dependencies) return jsonFailure(503, "private_chat_storage_unavailable");
|
|
180
196
|
|
|
181
|
-
const authorization = await principalForRequest(request, request.method === "POST" ? "console:write" : "console:read", options);
|
|
182
|
-
if (!authorization.principal) return jsonFailure(authorization.status, authorization.status === 401 ? "sign_in_required" : authorization.status === 403 ? "chat_account_access_denied" : "chat_runtime_unavailable");
|
|
183
|
-
|
|
184
197
|
const headers = new Headers({ accept: request.headers.get("accept") ?? "application/json" });
|
|
185
198
|
let body: string | undefined;
|
|
186
199
|
if (request.method === "POST" && isMessageRoute) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { newsroomChatFetch } from "./newsroom-chat-client";
|
|
1
2
|
export type PapyrusPrivateChatChannel = {
|
|
2
3
|
channel_id: string;
|
|
3
4
|
participants: Array<{ kind: string; actor_id: string }>;
|
|
@@ -39,8 +40,8 @@ async function requestJson<T>(path: string, init: RequestInit, fetcher: PapyrusP
|
|
|
39
40
|
return payload as T;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
/** Open the current editor's server-derived private channel using the
|
|
43
|
-
export async function openPapyrusPrivateChat(fetcher: PapyrusPrivateChatFetch =
|
|
43
|
+
/** Open the current editor's server-derived private channel using the verified newsroom identity and connected account session. */
|
|
44
|
+
export async function openPapyrusPrivateChat(fetcher: PapyrusPrivateChatFetch = newsroomChatFetch): Promise<PapyrusPrivateChatChannel> {
|
|
44
45
|
const result = await requestJson<PapyrusPrivateChatChannel>("private-channel", { method: "POST" }, fetcher);
|
|
45
46
|
if (!result.channel_id?.trim() || !Array.isArray(result.participants)) {
|
|
46
47
|
throw new Error("Private chat returned an invalid channel.");
|
|
@@ -51,7 +52,7 @@ export async function openPapyrusPrivateChat(fetcher: PapyrusPrivateChatFetch =
|
|
|
51
52
|
/** Read only the private channel returned by the host's authenticated open request. */
|
|
52
53
|
export async function listPapyrusPrivateChatMessages(
|
|
53
54
|
channelId: string,
|
|
54
|
-
fetcher: PapyrusPrivateChatFetch =
|
|
55
|
+
fetcher: PapyrusPrivateChatFetch = newsroomChatFetch,
|
|
55
56
|
): Promise<PapyrusPrivateChatMessage[]> {
|
|
56
57
|
if (!channelId.trim()) throw new Error("A private chat channel is required.");
|
|
57
58
|
const result = await requestJson<{ messages?: unknown }>(`channels/${encodeURIComponent(channelId)}/messages`, { method: "GET" }, fetcher);
|
|
@@ -64,7 +65,7 @@ export async function sendPapyrusPrivateChatMessage(
|
|
|
64
65
|
channelId: string,
|
|
65
66
|
body: string,
|
|
66
67
|
idempotencyKey: string,
|
|
67
|
-
fetcher: PapyrusPrivateChatFetch =
|
|
68
|
+
fetcher: PapyrusPrivateChatFetch = newsroomChatFetch,
|
|
68
69
|
): Promise<PapyrusPrivateChatSendResult> {
|
|
69
70
|
if (!channelId.trim() || !body.trim() || !idempotencyKey.trim()) {
|
|
70
71
|
throw new Error("A channel, message and idempotency key are required.");
|
|
@@ -33,6 +33,7 @@ const MINIMUM_SEALING_SECRET_LENGTH = 32;
|
|
|
33
33
|
|
|
34
34
|
export interface PendingProspectusSignInCookie extends PendingProspectusSignIn {
|
|
35
35
|
returnPath: string;
|
|
36
|
+
newsroomSubject?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface ProspectusSignedInSession {
|
|
@@ -40,6 +41,7 @@ export interface ProspectusSignedInSession {
|
|
|
40
41
|
accessToken: string;
|
|
41
42
|
accessTokenExpiresAt: number;
|
|
42
43
|
selectedAccountId?: string;
|
|
44
|
+
newsroomSubject?: string;
|
|
43
45
|
siteSubject?: string;
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveNewsroomChatIdentity, type NewsroomChatIdentityResolver } from "./newsroom-chat-identity";
|
|
1
2
|
import { NextResponse, type NextRequest } from "next/server";
|
|
2
3
|
import {
|
|
3
4
|
CYCLOTRON_LAB_PRODUCT,
|
|
@@ -36,6 +37,7 @@ import { SiteOriginNotConfiguredError, publicSiteOrigin, siteOriginNotConfigured
|
|
|
36
37
|
* unregistered deployment never half-signs anyone in.
|
|
37
38
|
*/
|
|
38
39
|
export interface ProspectusSignInRouteOptions {
|
|
40
|
+
resolveNewsroomIdentity?: NewsroomChatIdentityResolver;
|
|
39
41
|
resolveRuntime?: (siteOrigin: string) => Promise<ProspectusSignInRuntime | null>;
|
|
40
42
|
siteProduct?: string | null;
|
|
41
43
|
completionOverrides?: Pick<CompleteProspectusGoogleSignInInput, "verifyCognitoTokens" | "fetcher">;
|
|
@@ -137,12 +139,25 @@ export function handleProspectusSignInStart(request: NextRequest, options: Prosp
|
|
|
137
139
|
async function handleProspectusSignInStartRoute(request: NextRequest, options: ProspectusSignInRouteOptions): Promise<NextResponse> {
|
|
138
140
|
const runtime = await signInRuntimeFor(request, options);
|
|
139
141
|
if (!runtime) return signInUnavailableResponse();
|
|
142
|
+
const newsroomConnection = request.nextUrl.searchParams.get("newsroomChat") === "true";
|
|
143
|
+
let newsroomSubject: string | undefined;
|
|
144
|
+
if (newsroomConnection) {
|
|
145
|
+
if (request.method !== "POST" || request.headers.get("origin") !== siteOriginFor(request, options)) {
|
|
146
|
+
return signInFailedResponse(403, "same_origin_required", "Connect chat from this newsroom.");
|
|
147
|
+
}
|
|
148
|
+
const identity = await (options.resolveNewsroomIdentity ?? resolveNewsroomChatIdentity)(request).catch(() => null);
|
|
149
|
+
if (!identity) return signInFailedResponse(401, "sign_in_required", "An authenticated newsroom editor is required.");
|
|
150
|
+
newsroomSubject = identity.subject;
|
|
151
|
+
}
|
|
140
152
|
const { authorizationUrl, pendingSignIn } = await beginProspectusGoogleSignIn(runtime.accounts, runtime.cognitoClient);
|
|
141
153
|
const pendingSignInCookie: PendingProspectusSignInCookie = {
|
|
142
154
|
...pendingSignIn,
|
|
143
|
-
|
|
155
|
+
...(newsroomSubject ? { newsroomSubject } : {}),
|
|
156
|
+
returnPath: sameSiteReturnPath(newsroomConnection ? request.nextUrl.searchParams.get("returnTo") : await requestedReturnPath(request)),
|
|
144
157
|
};
|
|
145
|
-
const response =
|
|
158
|
+
const response = newsroomConnection
|
|
159
|
+
? NextResponse.json({ authorizationUrl }, { headers: NO_STORE_HEADERS })
|
|
160
|
+
: NextResponse.redirect(authorizationUrl, { status: 303, headers: NO_STORE_HEADERS });
|
|
146
161
|
response.cookies.set(
|
|
147
162
|
PROSPECTUS_PENDING_SIGN_IN_COOKIE,
|
|
148
163
|
sealProspectusCookie(runtime.sealingSecret, PROSPECTUS_PENDING_SIGN_IN_COOKIE, pendingSignInCookie, pendingSignIn.expiresAt),
|
|
@@ -194,11 +209,13 @@ async function completeCallback(
|
|
|
194
209
|
pendingSignIn,
|
|
195
210
|
returnedState,
|
|
196
211
|
authorizationCode,
|
|
212
|
+
expectedNewsroomSubject: pendingSignIn.newsroomSubject,
|
|
197
213
|
nowInSeconds,
|
|
198
214
|
...options.completionOverrides,
|
|
199
215
|
});
|
|
200
216
|
const session: ProspectusSignedInSession = {
|
|
201
217
|
personId: signedIn.person.id,
|
|
218
|
+
...(pendingSignIn.newsroomSubject ? { newsroomSubject: pendingSignIn.newsroomSubject } : {}),
|
|
202
219
|
accessToken: signedIn.accessToken,
|
|
203
220
|
accessTokenExpiresAt: signedIn.accessTokenExpiresAt,
|
|
204
221
|
...(signedIn.person.siteSubject ? { siteSubject: signedIn.person.siteSubject } : {}),
|
|
@@ -281,6 +281,7 @@ export interface CompleteProspectusGoogleSignInInput {
|
|
|
281
281
|
pendingSignIn: PendingProspectusSignIn;
|
|
282
282
|
returnedState: string;
|
|
283
283
|
authorizationCode: string;
|
|
284
|
+
expectedNewsroomSubject?: string;
|
|
284
285
|
verifyCognitoTokens?: CognitoTokenVerifier;
|
|
285
286
|
fetcher?: typeof fetch;
|
|
286
287
|
nowInSeconds?: number;
|
|
@@ -320,6 +321,12 @@ export async function completeProspectusGoogleSignIn(
|
|
|
320
321
|
expiresInSeconds: Number(tokenBody.expires_in) || 0,
|
|
321
322
|
};
|
|
322
323
|
await (input.verifyCognitoTokens ?? cognitoTokenVerifier(input.cognitoClient))(tokens, input.pendingSignIn.nonce);
|
|
324
|
+
if (input.expectedNewsroomSubject) {
|
|
325
|
+
const claims = JSON.parse(Buffer.from(tokens.accessToken.split(".")[1], "base64url").toString("utf8")) as { sub?: string };
|
|
326
|
+
if (claims.sub !== input.expectedNewsroomSubject) {
|
|
327
|
+
throw new ProspectusAccountsError(403, "newsroom_identity_mismatch", "Continue with the same Google account as your newsroom session.");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
323
330
|
const { person } = await input.accounts.resolveGoogleSignIn(input.pendingSignIn, tokens.accessToken, tokens.idToken);
|
|
324
331
|
return {
|
|
325
332
|
person,
|
package/package.json
CHANGED
|
@@ -257,6 +257,8 @@
|
|
|
257
257
|
"./lib/newsroom-board": "./lib/newsroom-board.ts",
|
|
258
258
|
"./lib/newsroom-card-layout": "./lib/newsroom-card-layout.ts",
|
|
259
259
|
"./lib/newsroom-category-drilldown": "./lib/newsroom-category-drilldown.ts",
|
|
260
|
+
"./lib/newsroom-chat-client": "./lib/newsroom-chat-client.ts",
|
|
261
|
+
"./lib/newsroom-chat-identity": "./lib/newsroom-chat-identity.ts",
|
|
260
262
|
"./lib/newsroom-demo-articles": "./lib/newsroom-demo-articles.ts",
|
|
261
263
|
"./lib/newsroom-demo-dashboard": "./lib/newsroom-demo-dashboard.ts",
|
|
262
264
|
"./lib/newsroom-demo-profile": "./lib/newsroom-demo-profile.ts",
|
|
@@ -456,7 +458,7 @@
|
|
|
456
458
|
"optional": true
|
|
457
459
|
}
|
|
458
460
|
},
|
|
459
|
-
"version": "5.41.
|
|
461
|
+
"version": "5.41.2",
|
|
460
462
|
"dependencies": {
|
|
461
463
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
462
464
|
"@aws-crypto/sha256-js": "5.2.0",
|
package/routes.manifest.json
CHANGED