@anthusai/papyrus 5.41.1 → 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.
@@ -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 refreshMessages = useCallback(async (channelId: string) => {
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
- setError(failureMessage(nextError));
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) setError(failureMessage(nextError));
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
- setError(failureMessage(nextError));
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
- }, [channel, sending]);
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
- <strong>Private chat could not be opened.</strong>
121
- <span>{error}</span>
122
- <button className="papyrus-console__new-session" onClick={() => void openConversation()} type="button">Retry</button>
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 || !session.selectedAccountId || !Number.isFinite(session.accessTokenExpiresAt) || session.accessTokenExpiresAt <= nowInSeconds) {
80
- return { principal: null, status: 401 };
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 signed same-origin session cookie. */
43
- export async function openPapyrusPrivateChat(fetcher: PapyrusPrivateChatFetch = globalThis.fetch): Promise<PapyrusPrivateChatChannel> {
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 = globalThis.fetch,
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 = globalThis.fetch,
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
- returnPath: sameSiteReturnPath(await requestedReturnPath(request)),
155
+ ...(newsroomSubject ? { newsroomSubject } : {}),
156
+ returnPath: sameSiteReturnPath(newsroomConnection ? request.nextUrl.searchParams.get("returnTo") : await requestedReturnPath(request)),
144
157
  };
145
- const response = NextResponse.redirect(authorizationUrl, { status: 303, headers: NO_STORE_HEADERS });
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.1",
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",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.41.1",
2
+ "version": "5.41.2",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",