@anthusai/papyrus 2.0.0-next.11 → 2.0.0-next.110
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/auth/resource.js +16 -1
- package/amplify/auth/resource.ts +19 -1
- package/amplify/data/function-handler.js +20 -0
- package/amplify/data/function-handler.ts +40 -0
- package/amplify/data/resource.js +152 -62
- package/amplify/data/resource.ts +158 -62
- package/amplify/data/schema.js +2101 -1987
- package/amplify/data/schema.ts +197 -70
- package/amplify/functions/assignment-action/data-access.js +2 -0
- package/amplify/functions/assignment-action/data-access.ts +2 -0
- package/amplify/functions/assignment-action/handler.ts +1 -1
- package/amplify/functions/automation-view/handler.py +17 -0
- package/amplify/functions/automation-view/resource.js +24 -0
- package/amplify/functions/automation-view/resource.ts +34 -0
- package/amplify/functions/automation-view/schema.js +26 -0
- package/amplify/functions/automation-view/schema.ts +28 -0
- package/amplify/functions/automation-view/wire.js +56 -0
- package/amplify/functions/automation-view/wire.ts +76 -0
- package/amplify/functions/content-actions/handler.py +64 -3
- package/amplify/functions/content-actions/requirements.txt +1 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/editorial-checks/handler.py +2 -2
- package/amplify/functions/editorial-checks/requirements.txt +1 -1
- package/amplify/functions/editorial-checks/resource.js +3 -3
- package/amplify/functions/editorial-checks/resource.ts +3 -3
- package/amplify/functions/editorial-checks/schema.js +38 -8
- package/amplify/functions/editorial-checks/schema.ts +38 -8
- package/amplify/functions/editorial-checks/wire.js +4 -0
- package/amplify/functions/editorial-checks/wire.ts +4 -0
- package/amplify/functions/monitoring-view/handler.py +11 -0
- package/amplify/functions/monitoring-view/resource.js +24 -0
- package/amplify/functions/monitoring-view/resource.ts +34 -0
- package/amplify/functions/monitoring-view/schema.js +29 -0
- package/amplify/functions/monitoring-view/schema.ts +31 -0
- package/amplify/functions/monitoring-view/wire.js +64 -0
- package/amplify/functions/monitoring-view/wire.ts +81 -0
- package/amplify/functions/public-forms/core.js +247 -0
- package/amplify/functions/public-forms/core.ts +310 -0
- package/amplify/functions/public-forms/handler.ts +41 -0
- package/amplify/functions/public-forms/origins.js +35 -0
- package/amplify/functions/public-forms/origins.ts +43 -0
- package/amplify/functions/public-forms/resource.js +9 -0
- package/amplify/functions/public-forms/resource.ts +10 -0
- package/amplify/functions/public-forms/wire.js +25 -0
- package/amplify/functions/public-forms/wire.ts +32 -0
- package/amplify/functions/shared/editorial-context.ts +63 -4
- package/amplify/functions/shared/python-bundle.js +20 -14
- package/amplify/functions/shared/python-bundle.ts +25 -18
- package/amplify/plugins/newsletter/config.js +245 -0
- package/amplify/plugins/newsletter/config.ts +305 -0
- package/amplify/plugins/newsletter/function/handler.py +6 -0
- package/amplify/plugins/newsletter/function/requirements.txt +5 -0
- package/amplify/plugins/newsletter/ops/handler.py +12 -0
- package/amplify/plugins/newsletter/ops/resource.js +27 -0
- package/amplify/plugins/newsletter/ops/resource.ts +37 -0
- package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
- package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
- package/amplify/plugins/newsletter/schema.js +57 -0
- package/amplify/plugins/newsletter/schema.ts +62 -0
- package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
- package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
- package/amplify/plugins/newsletter/sending.js +140 -0
- package/amplify/plugins/newsletter/sending.ts +186 -0
- package/amplify/plugins/reader-steering/config.js +182 -0
- package/amplify/plugins/reader-steering/config.ts +290 -0
- package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
- package/amplify/plugins/reader-steering/function/handler.py +1589 -0
- package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
- package/amplify/plugins/reader-steering/function/moderation.py +316 -0
- package/amplify/plugins/reader-steering/function/proposals.py +487 -0
- package/amplify/plugins/reader-steering/function/resource.js +27 -0
- package/amplify/plugins/reader-steering/function/resource.ts +35 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +187 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +213 -0
- package/amplify/plugins/reader-steering/schema.js +252 -0
- package/amplify/plugins/reader-steering/schema.ts +254 -0
- package/amplify/plugins/registry.js +4 -0
- package/amplify/plugins/registry.ts +4 -0
- package/amplify/plugins/signup/schema.js +2 -1
- package/amplify/plugins/signup/schema.ts +2 -1
- package/amplify/plugins/types.ts +5 -0
- package/amplify/plugins/wire.js +1 -0
- package/amplify/plugins/wire.ts +1 -0
- package/amplify/publication/projection-lease.js +30 -0
- package/amplify/publication/projection-lease.ts +39 -0
- package/amplify/publication/redirect-and-index.js +105 -0
- package/amplify/publication/redirect-rules.js +73 -0
- package/amplify/publication/redirect-rules.ts +93 -0
- package/amplify/publication/s3-projection.js +7 -1
- package/amplify/publication/s3-projection.ts +9 -1
- package/amplify/site-backend-config.js +19 -0
- package/amplify/site-backend-config.ts +45 -1
- package/amplify/site-backend-names.js +4 -2
- package/amplify/site-backend-names.ts +4 -2
- package/amplify/site-backend.js +42 -4
- package/amplify/site-backend.ts +45 -4
- package/app/.well-known/papyrus-cli.json/route.ts +9 -0
- package/app/api/auth/prospectus/sign-in/route.ts +4 -0
- package/app/api/newsletter/confirm/route.ts +13 -0
- package/app/api/newsletter/preferences/route.ts +13 -0
- package/app/api/newsletter/state/route.ts +9 -0
- package/app/api/newsletter/subscribe/route.ts +9 -0
- package/app/api/newsletter/unsubscribe/route.ts +13 -0
- package/app/api/newsroom/chat/[...path]/route.ts +17 -0
- package/app/api/newsroom/curation/context/route.ts +9 -0
- package/app/api/newsroom/curation/priming/route.ts +9 -0
- package/app/api/newsroom/curation/publication/route.ts +9 -0
- package/app/api/reader/comment/delete/route.ts +9 -0
- package/app/api/reader/comment/edit/route.ts +9 -0
- package/app/api/reader/comment/route.ts +9 -0
- package/app/api/reader/flag/route.ts +9 -0
- package/app/api/reader/handle/route.ts +9 -0
- package/app/api/reader/me/route.ts +9 -0
- package/app/api/reader/rating/route.ts +9 -0
- package/app/api/reader/report/route.ts +9 -0
- package/app/api/reader/retired/route.ts +9 -0
- package/app/api/reader/submit/route.ts +9 -0
- package/app/api/reader/vote/route.ts +9 -0
- package/app/layout.tsx +5 -1
- package/bin/papyrus-infra.mjs +20 -1
- package/components/automation-panel.tsx +49 -0
- package/components/automation-run-now.tsx +97 -0
- package/components/automation-view.tsx +195 -0
- package/components/editorial-checks-admin-panel.tsx +32 -17
- package/components/moderation-panel.tsx +248 -0
- package/components/monitoring-overview-view.tsx +422 -0
- package/components/monitoring-panel.tsx +60 -0
- package/components/news-desk-client-provider.tsx +18 -18
- package/components/news-desk-taxonomy-client.ts +28 -8
- package/components/newsletter-panel.tsx +474 -0
- package/components/newsletter-sections.tsx +163 -0
- package/components/newsroom-access-gate.tsx +16 -2
- package/components/newsroom-article-editor.tsx +9 -7
- package/components/newsroom-articles-view.tsx +6 -6
- package/components/newsroom-assignment-detail.tsx +6 -6
- package/components/newsroom-board-view.tsx +8 -3
- package/components/newsroom-curation-bootstrap.tsx +45 -0
- package/components/newsroom-ops-overview.tsx +26 -96
- package/components/newsroom-ops-shell.tsx +2 -2
- package/components/newsroom-organizer.css +33 -0
- package/components/newsroom-overview-skeleton.tsx +25 -0
- package/components/newsroom-references-view.tsx +192 -49
- package/components/notification-center.tsx +281 -0
- package/components/papyrus-console-conversation.css +14 -0
- package/components/papyrus-console-conversation.tsx +55 -0
- package/components/papyrus-console-shell.tsx +62 -420
- package/components/papyrus-private-chat-panel.tsx +134 -0
- package/components/publication-cyclotron-panel.tsx +15 -10
- package/components/reader-auth-state.ts +1 -1
- package/components/reference-add-entry.tsx +75 -0
- package/components/reference-curation-controls.tsx +31 -22
- package/components/reference-cyclotron-priming.tsx +58 -19
- package/components/reference-cyclotron-suggestion.tsx +10 -6
- package/components/reference-priming.tsx +14 -10
- package/components/reference-relevance-review.tsx +3 -3
- package/components/reference-section-suggestion.tsx +1 -1
- package/components/topic-steering-workspace.tsx +230 -144
- package/infra/amplify-app-shell.js +96 -16
- package/infra/amplify-app-shell.ts +108 -18
- package/infra/cloud-email-config.js +32 -0
- package/infra/cloud-email-config.ts +39 -0
- package/infra/cloud-email.js +57 -0
- package/infra/cloud-email.test.js +69 -0
- package/infra/cloud-email.test.ts +66 -0
- package/infra/cloud-email.ts +60 -0
- package/infra/index.js +1 -0
- package/infra/secure-string-parameter-handler.js +41 -0
- package/infra/secure-string-parameter-handler.ts +41 -0
- package/infra/secure-string-parameter.js +79 -0
- package/infra/secure-string-parameter.ts +91 -0
- package/infra/site-config.js +92 -6
- package/infra/site-config.ts +138 -6
- package/lib/automation-client.ts +54 -0
- package/lib/automation-fixture.ts +93 -0
- package/lib/automation-overview.ts +252 -0
- package/lib/category-repository.ts +14 -12
- package/lib/cli-login-discovery.ts +78 -0
- package/lib/console-web-context.ts +1 -1
- package/lib/curation-routes.ts +345 -0
- package/lib/cyclotron-suggestions.ts +4 -4
- package/lib/editorial-checks-client.ts +18 -0
- package/lib/editorial-checks-demo.ts +1 -1
- package/lib/editorial-checks.ts +8 -4
- package/lib/editorial-preview.ts +5 -5
- package/lib/monitoring-client.ts +83 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +191 -0
- package/lib/news-desk-session.ts +12 -21
- package/lib/newsletter-api.ts +418 -0
- package/lib/newsletter-client.ts +46 -0
- package/lib/newsletter-fixture.ts +184 -0
- package/lib/newsletter-model.ts +202 -0
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +2 -1
- package/lib/papyrus-chat-admission.ts +65 -0
- package/lib/papyrus-hosted-chat.ts +217 -0
- package/lib/papyrus-private-chat-client.ts +81 -0
- package/lib/prospectus-session.ts +30 -14
- package/lib/prospectus-sign-in-routes.ts +106 -31
- package/lib/prospectus-site-identity.ts +46 -6
- package/lib/reader-api.ts +145 -0
- package/lib/reader-moderation.ts +220 -0
- package/lib/reader-retirement-route.ts +89 -0
- package/lib/reader-subject.md +29 -0
- package/lib/reader-subject.ts +53 -0
- package/lib/reference-review-details.ts +71 -0
- package/lib/reference-review-request.ts +14 -0
- package/lib/relevance-decisions.ts +42 -10
- package/lib/relevance-status.ts +2 -2
- package/lib/review-client.ts +120 -0
- package/lib/review-fixture.ts +45 -0
- package/lib/review-requests.ts +246 -0
- package/lib/semantic-graph.ts +2 -0
- package/lib/shared-chat-admission.ts +182 -0
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +6 -3
- package/lib/site-origin-value.ts +50 -0
- package/lib/site-origin.ts +8 -0
- package/lib/use-reference-review-data.ts +44 -0
- package/middleware.ts +14 -3
- package/package.json +67 -3
- package/routes.manifest.json +238 -2
- package/src/sync.mjs +11 -2
- package/src/with-papyrus.d.mts +1 -0
- package/src/with-papyrus.mjs +17 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { EmbeddedChatAppDependencies } from "chatticus/chat-server";
|
|
3
|
+
import type { NextRequest } from "next/server";
|
|
4
|
+
import {
|
|
5
|
+
ProspectusAccountsError,
|
|
6
|
+
authorizeSignedInPersonForAccount,
|
|
7
|
+
} from "./prospectus-site-identity";
|
|
8
|
+
import {
|
|
9
|
+
resolveProspectusSignInRuntime,
|
|
10
|
+
signedInProspectusSession,
|
|
11
|
+
type ProspectusSignInRuntime,
|
|
12
|
+
} from "./prospectus-session";
|
|
13
|
+
import { prospectusSiteProduct, publishingAccountsForSite } from "./prospectus-publication-access";
|
|
14
|
+
import { publicSiteOrigin } from "./site-origin-value";
|
|
15
|
+
|
|
16
|
+
const NO_STORE_HEADERS = { "cache-control": "no-store" };
|
|
17
|
+
|
|
18
|
+
export interface PapyrusChatScope {
|
|
19
|
+
siteProduct: string;
|
|
20
|
+
accountId: string;
|
|
21
|
+
personId: string;
|
|
22
|
+
tenantId: string;
|
|
23
|
+
editorId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PapyrusHostedChatOptions {
|
|
27
|
+
resolveRuntime?: (siteOrigin: string) => Promise<ProspectusSignInRuntime | null>;
|
|
28
|
+
resolvePrivateDependencies?: () => Promise<EmbeddedChatAppDependencies | null>;
|
|
29
|
+
privateChatEnabled?: boolean;
|
|
30
|
+
siteProduct?: string | null;
|
|
31
|
+
environment?: Record<string, string | undefined>;
|
|
32
|
+
nowInSeconds?: () => number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function siteAndAccountKey(siteProduct: string, accountId: string): string {
|
|
36
|
+
return createHash("sha256").update(JSON.stringify([siteProduct, accountId])).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function derivePapyrusChatScope(siteProduct: string, accountId: string, personId: string): PapyrusChatScope {
|
|
40
|
+
const tenantHash = siteAndAccountKey(siteProduct, accountId);
|
|
41
|
+
const editorHash = createHash("sha256").update(JSON.stringify([siteProduct, accountId, personId])).digest("hex");
|
|
42
|
+
return {
|
|
43
|
+
siteProduct,
|
|
44
|
+
personId,
|
|
45
|
+
accountId,
|
|
46
|
+
tenantId: `papyrus-${tenantHash.slice(0, 40)}`,
|
|
47
|
+
editorId: `papyrus-editor-${editorHash.slice(0, 40)}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ResolvedPapyrusPrincipal = {
|
|
52
|
+
kind: "user";
|
|
53
|
+
tenantId: string;
|
|
54
|
+
userId: string;
|
|
55
|
+
workerId: null;
|
|
56
|
+
organizationStatus: "enabled";
|
|
57
|
+
role: "member";
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
async function principalForRequest(
|
|
61
|
+
request: NextRequest,
|
|
62
|
+
operation: "console:read" | "console:write",
|
|
63
|
+
options: PapyrusHostedChatOptions,
|
|
64
|
+
): Promise<{ principal: ResolvedPapyrusPrincipal | null; status: number }> {
|
|
65
|
+
const environment = options.environment ?? process.env;
|
|
66
|
+
const siteProduct = options.siteProduct !== undefined ? options.siteProduct : prospectusSiteProduct(environment);
|
|
67
|
+
if (!siteProduct) return { principal: null, status: 503 };
|
|
68
|
+
|
|
69
|
+
let runtime: ProspectusSignInRuntime | null;
|
|
70
|
+
try {
|
|
71
|
+
runtime = await (options.resolveRuntime ?? resolveProspectusSignInRuntime)(publicSiteOrigin(request, environment));
|
|
72
|
+
} catch {
|
|
73
|
+
return { principal: null, status: 503 };
|
|
74
|
+
}
|
|
75
|
+
if (!runtime) return { principal: null, status: 503 };
|
|
76
|
+
|
|
77
|
+
const nowInSeconds = options.nowInSeconds?.() ?? Math.floor(Date.now() / 1000);
|
|
78
|
+
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 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const directory = await runtime.accounts.accountDirectory(session.accessToken);
|
|
85
|
+
if (directory.person?.id !== session.personId) return { principal: null, status: 401 };
|
|
86
|
+
if (!publishingAccountsForSite(directory.accounts, siteProduct).some((account) => account.accountId === session.selectedAccountId)) {
|
|
87
|
+
return { principal: null, status: 403 };
|
|
88
|
+
}
|
|
89
|
+
const authorization = await authorizeSignedInPersonForAccount(
|
|
90
|
+
runtime.accounts,
|
|
91
|
+
session.accessToken,
|
|
92
|
+
session.selectedAccountId,
|
|
93
|
+
operation,
|
|
94
|
+
);
|
|
95
|
+
if (authorization.personId !== session.personId || authorization.accountId !== session.selectedAccountId || authorization.operation !== operation) {
|
|
96
|
+
return { principal: null, status: 403 };
|
|
97
|
+
}
|
|
98
|
+
const scope = derivePapyrusChatScope(siteProduct, session.selectedAccountId, session.personId);
|
|
99
|
+
return {
|
|
100
|
+
principal: {
|
|
101
|
+
kind: "user",
|
|
102
|
+
tenantId: scope.tenantId,
|
|
103
|
+
userId: scope.editorId,
|
|
104
|
+
workerId: null,
|
|
105
|
+
organizationStatus: "enabled",
|
|
106
|
+
role: "member",
|
|
107
|
+
},
|
|
108
|
+
status: 200,
|
|
109
|
+
};
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof ProspectusAccountsError && (error.status === 401 || error.status === 403)) {
|
|
112
|
+
return { principal: null, status: error.status };
|
|
113
|
+
}
|
|
114
|
+
return { principal: null, status: 503 };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function jsonFailure(status: number, error: string): Response {
|
|
119
|
+
return new Response(JSON.stringify({ error }), {
|
|
120
|
+
status,
|
|
121
|
+
headers: { ...NO_STORE_HEADERS, "content-type": "application/json" },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function supportedPath(path: readonly string[]): boolean {
|
|
126
|
+
return (path.length === 1 && path[0] === "private-channel") ||
|
|
127
|
+
(path.length === 3 && path[0] === "channels" && path[2] === "messages") ||
|
|
128
|
+
(path.length === 3 && path[0] === "turns" && (path[2] === "events" || path[2] === "stream"));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function chatRequestPath(path: readonly string[]): string {
|
|
132
|
+
return `/${path.map((part) => encodeURIComponent(part)).join("/")}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function handlePapyrusHostedChatRequest(
|
|
136
|
+
request: NextRequest,
|
|
137
|
+
path: readonly string[],
|
|
138
|
+
options: PapyrusHostedChatOptions = {},
|
|
139
|
+
): Promise<Response> {
|
|
140
|
+
if (!supportedPath(path) || path.some((part) => !part || part === "." || part === ".." || part.includes("/"))) {
|
|
141
|
+
return jsonFailure(404, "chat_route_not_found");
|
|
142
|
+
}
|
|
143
|
+
if (request.method !== "GET" && request.method !== "POST") return jsonFailure(405, "method_not_allowed");
|
|
144
|
+
const isMessageRoute = path[0] === "channels";
|
|
145
|
+
const isPrivateChannelRoute = path[0] === "private-channel";
|
|
146
|
+
if (isPrivateChannelRoute && request.method !== "POST") return jsonFailure(405, "method_not_allowed");
|
|
147
|
+
if ((isMessageRoute && path[2] === "messages" && request.method === "POST") || (!isMessageRoute && !isPrivateChannelRoute && request.method !== "GET")) {
|
|
148
|
+
if (!isMessageRoute && !isPrivateChannelRoute) return jsonFailure(405, "method_not_allowed");
|
|
149
|
+
}
|
|
150
|
+
if (isMessageRoute && request.method === "GET" && path[2] !== "messages") return jsonFailure(405, "method_not_allowed");
|
|
151
|
+
if ((isMessageRoute || isPrivateChannelRoute) && request.method === "POST") {
|
|
152
|
+
let origin: string;
|
|
153
|
+
try {
|
|
154
|
+
origin = publicSiteOrigin(request, options.environment ?? process.env);
|
|
155
|
+
} catch {
|
|
156
|
+
return jsonFailure(503, "chat_site_origin_unavailable");
|
|
157
|
+
}
|
|
158
|
+
if (request.headers.get("origin") !== origin) return jsonFailure(403, "same_origin_required");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const environment = options.environment ?? process.env;
|
|
162
|
+
const privateChatEnabled = options.privateChatEnabled ?? environment.PAPYRUS_PRIVATE_CHAT_ENABLED === "true";
|
|
163
|
+
if (!privateChatEnabled) return jsonFailure(404, "private_chat_disabled");
|
|
164
|
+
|
|
165
|
+
let dependencies: EmbeddedChatAppDependencies | null;
|
|
166
|
+
try {
|
|
167
|
+
if (options.resolvePrivateDependencies) {
|
|
168
|
+
dependencies = await options.resolvePrivateDependencies();
|
|
169
|
+
} else {
|
|
170
|
+
const chatRuntime = await import("chatticus/chat-runtime");
|
|
171
|
+
dependencies = chatRuntime.createSiteHostedChatDependencies(
|
|
172
|
+
chatRuntime.siteHostedChatResourcesFromEnvironment(environment),
|
|
173
|
+
chatRuntime.siteHostedChatClients(),
|
|
174
|
+
) as EmbeddedChatAppDependencies;
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
dependencies = null;
|
|
178
|
+
}
|
|
179
|
+
if (!dependencies) return jsonFailure(503, "private_chat_storage_unavailable");
|
|
180
|
+
|
|
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
|
+
const headers = new Headers({ accept: request.headers.get("accept") ?? "application/json" });
|
|
185
|
+
let body: string | undefined;
|
|
186
|
+
if (request.method === "POST" && isMessageRoute) {
|
|
187
|
+
const idempotencyKey = request.headers.get("idempotency-key")?.trim();
|
|
188
|
+
const input = await request.json().catch(() => null) as Record<string, unknown> | null;
|
|
189
|
+
const privateBotId = environment.PAPYRUS_CHAT_PRIVATE_BOT_ID?.trim();
|
|
190
|
+
if (!idempotencyKey || typeof input?.body !== "string" || !input.body.trim()) {
|
|
191
|
+
return jsonFailure(422, "message_body_and_idempotency_key_required");
|
|
192
|
+
}
|
|
193
|
+
if (!privateBotId) return jsonFailure(503, "private_chat_storage_unavailable");
|
|
194
|
+
headers.set("content-type", "application/json");
|
|
195
|
+
headers.set("idempotency-key", idempotencyKey);
|
|
196
|
+
body = JSON.stringify({
|
|
197
|
+
author_kind: "human",
|
|
198
|
+
author_id: authorization.principal.userId,
|
|
199
|
+
body: input.body,
|
|
200
|
+
addressed_to_bot_id: privateBotId,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const target = new URL(chatRequestPath(path) + new URL(request.url).search, request.url);
|
|
205
|
+
const forwarded = new Request(target, { method: request.method, headers, ...(body === undefined ? {} : { body }) });
|
|
206
|
+
let chatServer: typeof import("chatticus/chat-server");
|
|
207
|
+
try {
|
|
208
|
+
chatServer = await import("chatticus/chat-server");
|
|
209
|
+
} catch {
|
|
210
|
+
return jsonFailure(503, "private_chat_storage_unavailable");
|
|
211
|
+
}
|
|
212
|
+
const app = chatServer.createEmbeddedChatApp({
|
|
213
|
+
...dependencies,
|
|
214
|
+
resolvePrincipal: async () => authorization.principal,
|
|
215
|
+
});
|
|
216
|
+
return app.fetch(forwarded);
|
|
217
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type PapyrusPrivateChatChannel = {
|
|
2
|
+
channel_id: string;
|
|
3
|
+
participants: Array<{ kind: string; actor_id: string }>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type PapyrusPrivateChatMessage = {
|
|
7
|
+
message_id: string;
|
|
8
|
+
channel_id: string;
|
|
9
|
+
seq: number;
|
|
10
|
+
author_kind: "human" | "bot";
|
|
11
|
+
body: string;
|
|
12
|
+
addressed_to_bot_id: string | null;
|
|
13
|
+
created_at: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PapyrusPrivateChatSendResult = {
|
|
17
|
+
message: PapyrusPrivateChatMessage;
|
|
18
|
+
turn_id: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type PapyrusPrivateChatFetch = typeof globalThis.fetch;
|
|
22
|
+
|
|
23
|
+
const CHAT_API = "/api/newsroom/chat";
|
|
24
|
+
|
|
25
|
+
async function requestJson<T>(path: string, init: RequestInit, fetcher: PapyrusPrivateChatFetch): Promise<T> {
|
|
26
|
+
const response = await fetcher(`${CHAT_API}/${path}`, {
|
|
27
|
+
...init,
|
|
28
|
+
credentials: "same-origin",
|
|
29
|
+
cache: "no-store",
|
|
30
|
+
});
|
|
31
|
+
const payload = await response.json().catch(() => null) as Record<string, unknown> | null;
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const error = typeof payload?.error === "string" ? payload.error
|
|
34
|
+
: typeof payload?.detail === "string" ? payload.detail
|
|
35
|
+
: `Private chat request failed (${response.status}).`;
|
|
36
|
+
throw new Error(error);
|
|
37
|
+
}
|
|
38
|
+
if (!payload || typeof payload !== "object") throw new Error("Private chat returned an invalid response.");
|
|
39
|
+
return payload as T;
|
|
40
|
+
}
|
|
41
|
+
|
|
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> {
|
|
44
|
+
const result = await requestJson<PapyrusPrivateChatChannel>("private-channel", { method: "POST" }, fetcher);
|
|
45
|
+
if (!result.channel_id?.trim() || !Array.isArray(result.participants)) {
|
|
46
|
+
throw new Error("Private chat returned an invalid channel.");
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Read only the private channel returned by the host's authenticated open request. */
|
|
52
|
+
export async function listPapyrusPrivateChatMessages(
|
|
53
|
+
channelId: string,
|
|
54
|
+
fetcher: PapyrusPrivateChatFetch = globalThis.fetch,
|
|
55
|
+
): Promise<PapyrusPrivateChatMessage[]> {
|
|
56
|
+
if (!channelId.trim()) throw new Error("A private chat channel is required.");
|
|
57
|
+
const result = await requestJson<{ messages?: unknown }>(`channels/${encodeURIComponent(channelId)}/messages`, { method: "GET" }, fetcher);
|
|
58
|
+
if (!Array.isArray(result.messages)) throw new Error("Private chat returned an invalid message list.");
|
|
59
|
+
return result.messages as PapyrusPrivateChatMessage[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One explicit send. The browser sends neither its tenant nor author or bot identity. */
|
|
63
|
+
export async function sendPapyrusPrivateChatMessage(
|
|
64
|
+
channelId: string,
|
|
65
|
+
body: string,
|
|
66
|
+
idempotencyKey: string,
|
|
67
|
+
fetcher: PapyrusPrivateChatFetch = globalThis.fetch,
|
|
68
|
+
): Promise<PapyrusPrivateChatSendResult> {
|
|
69
|
+
if (!channelId.trim() || !body.trim() || !idempotencyKey.trim()) {
|
|
70
|
+
throw new Error("A channel, message and idempotency key are required.");
|
|
71
|
+
}
|
|
72
|
+
const result = await requestJson<PapyrusPrivateChatSendResult>(`channels/${encodeURIComponent(channelId)}/messages`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "content-type": "application/json", "idempotency-key": idempotencyKey },
|
|
75
|
+
body: JSON.stringify({ body }),
|
|
76
|
+
}, fetcher);
|
|
77
|
+
if (!result.message?.message_id || result.message.channel_id !== channelId) {
|
|
78
|
+
throw new Error("Private chat returned an invalid sent message.");
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
3
|
+
import type { NextRequest } from "next/server";
|
|
3
4
|
import { readAmplifyOutputsFile } from "./amplify-outputs-path";
|
|
4
5
|
import {
|
|
5
6
|
ProspectusSiteAccounts,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
export const PROSPECTUS_PENDING_SIGN_IN_COOKIE = "__Host-papyrus-prospectus-pending";
|
|
22
23
|
export const PROSPECTUS_SESSION_COOKIE = "__Host-papyrus-prospectus-session";
|
|
23
24
|
export const PROSPECTUS_ACCOUNTS_ENDPOINT_ENV = "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT";
|
|
24
|
-
export const
|
|
25
|
+
export const PROSPECTUS_SESSION_SECRET_PARAMETER_ENV = "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER";
|
|
25
26
|
export const PROSPECTUS_SESSION_SECRET_CACHE_MILLISECONDS = 5 * 60 * 1000;
|
|
26
27
|
|
|
27
28
|
const SEALED_COOKIE_FORMAT_VERSION = "v1";
|
|
@@ -39,6 +40,21 @@ export interface ProspectusSignedInSession {
|
|
|
39
40
|
accessToken: string;
|
|
40
41
|
accessTokenExpiresAt: number;
|
|
41
42
|
selectedAccountId?: string;
|
|
43
|
+
siteSubject?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Open the existing sealed Prospectus session cookie for one server request. */
|
|
47
|
+
export function signedInProspectusSession(
|
|
48
|
+
request: Pick<NextRequest, "cookies">,
|
|
49
|
+
sealingSecret: string,
|
|
50
|
+
nowInSeconds?: number,
|
|
51
|
+
): ProspectusSignedInSession | null {
|
|
52
|
+
return openProspectusCookie<ProspectusSignedInSession>(
|
|
53
|
+
sealingSecret,
|
|
54
|
+
PROSPECTUS_SESSION_COOKIE,
|
|
55
|
+
request.cookies.get(PROSPECTUS_SESSION_COOKIE)?.value,
|
|
56
|
+
nowInSeconds,
|
|
57
|
+
);
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
function sealingKeyForCookie(sealingSecret: string, cookieName: string): Buffer {
|
|
@@ -101,37 +117,37 @@ export function sameSiteReturnPath(requestedReturnPath: string | null | undefine
|
|
|
101
117
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
102
118
|
}
|
|
103
119
|
|
|
104
|
-
export type ProspectusSessionSecretReader = (
|
|
120
|
+
export type ProspectusSessionSecretReader = (parameterName: string) => Promise<string | null>;
|
|
105
121
|
|
|
106
|
-
type CachedSealingSecret = {
|
|
122
|
+
type CachedSealingSecret = { parameterName: string; value: string; expiresAt: number };
|
|
107
123
|
|
|
108
124
|
let cachedSealingSecret: CachedSealingSecret | null = null;
|
|
109
|
-
let
|
|
125
|
+
let defaultSsmClient: SSMClient | null = null;
|
|
110
126
|
|
|
111
127
|
export function resetProspectusSessionSecretCache(): void {
|
|
112
128
|
cachedSealingSecret = null;
|
|
113
129
|
}
|
|
114
130
|
|
|
115
|
-
async function
|
|
116
|
-
|
|
117
|
-
const response = await
|
|
118
|
-
return response.
|
|
131
|
+
export async function readSealingSecretFromSsm(parameterName: string, client?: Pick<SSMClient, "send">): Promise<string | null> {
|
|
132
|
+
defaultSsmClient ??= new SSMClient({});
|
|
133
|
+
const response = await (client ?? defaultSsmClient).send(new GetParameterCommand({ Name: parameterName, WithDecryption: true }));
|
|
134
|
+
return response.Parameter?.Value?.trim() || null;
|
|
119
135
|
}
|
|
120
136
|
|
|
121
137
|
export async function loadProspectusSessionSealingSecret(
|
|
122
138
|
options: { environment?: Record<string, string | undefined>; readSecret?: ProspectusSessionSecretReader; now?: number } = {},
|
|
123
139
|
): Promise<string | null> {
|
|
124
140
|
const environment = options.environment ?? process.env;
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
141
|
+
const parameterName = environment[PROSPECTUS_SESSION_SECRET_PARAMETER_ENV]?.trim();
|
|
142
|
+
if (!parameterName) return null;
|
|
127
143
|
const now = options.now ?? Date.now();
|
|
128
|
-
if (cachedSealingSecret && cachedSealingSecret.
|
|
144
|
+
if (cachedSealingSecret && cachedSealingSecret.parameterName === parameterName && cachedSealingSecret.expiresAt > now) {
|
|
129
145
|
return cachedSealingSecret.value;
|
|
130
146
|
}
|
|
131
147
|
try {
|
|
132
|
-
const value = (await (options.readSecret ??
|
|
148
|
+
const value = (await (options.readSecret ?? readSealingSecretFromSsm)(parameterName))?.trim();
|
|
133
149
|
if (!value || value.length < MINIMUM_SEALING_SECRET_LENGTH) return null;
|
|
134
|
-
cachedSealingSecret = {
|
|
150
|
+
cachedSealingSecret = { parameterName, value, expiresAt: now + PROSPECTUS_SESSION_SECRET_CACHE_MILLISECONDS };
|
|
135
151
|
return value;
|
|
136
152
|
} catch (error) {
|
|
137
153
|
console.error(`Prospectus session sealing secret could not be read (${error instanceof Error ? error.name : "error"}).`);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
PROSPECTUS_SESSION_COOKIE,
|
|
13
13
|
openProspectusCookie,
|
|
14
14
|
resolveProspectusSignInRuntime,
|
|
15
|
+
signedInProspectusSession,
|
|
15
16
|
sameSiteReturnPath,
|
|
16
17
|
sealProspectusCookie,
|
|
17
18
|
sealedCookieAttributes,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
publishingAccountsForSite,
|
|
26
27
|
type PublicationCapability,
|
|
27
28
|
} from "./prospectus-publication-access";
|
|
29
|
+
import { SiteOriginNotConfiguredError, publicSiteOrigin, siteOriginNotConfiguredResponse } from "./site-origin";
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* The site's sign-in routes for Prospectus accounts: start (challenge, then the
|
|
@@ -38,6 +40,10 @@ export interface ProspectusSignInRouteOptions {
|
|
|
38
40
|
siteProduct?: string | null;
|
|
39
41
|
completionOverrides?: Pick<CompleteProspectusGoogleSignInInput, "verifyCognitoTokens" | "fetcher">;
|
|
40
42
|
nowInSeconds?: () => number;
|
|
43
|
+
/** Trusted configuration the public site origin is read from; defaults to process.env. */
|
|
44
|
+
environment?: Record<string, string | undefined>;
|
|
45
|
+
/** Receives each sign-in failure line; defaults to console.error so it reaches the compute log. */
|
|
46
|
+
log?: (line: string) => void;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
const NO_STORE_HEADERS = { "cache-control": "no-store" };
|
|
@@ -50,8 +56,22 @@ function currentTimeInSeconds(options: ProspectusSignInRouteOptions): number {
|
|
|
50
56
|
return options.nowInSeconds?.() ?? Math.floor(Date.now() / 1000);
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
function siteOriginFor(request: NextRequest, options: ProspectusSignInRouteOptions): string {
|
|
60
|
+
return publicSiteOrigin(request, options.environment);
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
async function signInRuntimeFor(request: NextRequest, options: ProspectusSignInRouteOptions): Promise<ProspectusSignInRuntime | null> {
|
|
54
|
-
return (options.resolveRuntime ?? resolveProspectusSignInRuntime)(request
|
|
64
|
+
return (options.resolveRuntime ?? resolveProspectusSignInRuntime)(siteOriginFor(request, options));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Runs a route and answers a missing site origin with 503 site_origin_not_configured, never a localhost redirect. */
|
|
68
|
+
async function failingClosedWithoutSiteOrigin(route: () => Promise<NextResponse>): Promise<NextResponse> {
|
|
69
|
+
try {
|
|
70
|
+
return await route();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof SiteOriginNotConfiguredError) return siteOriginNotConfiguredResponse(error);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
function signInUnavailableResponse(): NextResponse {
|
|
@@ -65,30 +85,62 @@ function clearSealedCookie(response: NextResponse, cookieName: string): void {
|
|
|
65
85
|
response.cookies.set(cookieName, "", sealedCookieAttributes(0, 0));
|
|
66
86
|
}
|
|
67
87
|
|
|
88
|
+
/** A JSON failure, for the routes the page script calls with fetch (session, account selection and creation). */
|
|
68
89
|
function signInFailedResponse(status: number, code: string, message: string): NextResponse {
|
|
69
90
|
return NextResponse.json({ error: code, message }, { status, headers: NO_STORE_HEADERS });
|
|
70
91
|
}
|
|
71
92
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
/**
|
|
94
|
+
* A failed sign-in never leaves the browser on /api/auth/prospectus/callback: it answers 303 to the page the reader started from
|
|
95
|
+
* (or the home page when that is not known) with `sign_in_error=<code>`, which the reader plugin turns into a short message.
|
|
96
|
+
*/
|
|
97
|
+
/** Which callback step failed; with the code it tells an operator where a reader's sign-in stopped. */
|
|
98
|
+
type SignInFailureBranch = "pending_cookie" | "provider_error" | "missing_parameters" | "completion" | "unexpected";
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* One structured line per failed callback: `prospectus_sign_in_failed {"code":...,"branch":...}`. It carries only the
|
|
102
|
+
* code, the branch, and (for errors from completing the sign-in) the HTTP status, Prospectus error code, its rule text
|
|
103
|
+
* (`prospectusMessage`, which names the broken rule and carries no personal data) and error name.
|
|
104
|
+
* Never tokens, authorization codes, state, emails or subjects.
|
|
105
|
+
*/
|
|
106
|
+
function logSignInFailure(
|
|
107
|
+
options: ProspectusSignInRouteOptions,
|
|
108
|
+
entry: { code: string; branch: SignInFailureBranch; httpStatus?: number; prospectusError?: string; prospectusMessage?: string; errorName?: string },
|
|
109
|
+
): void {
|
|
110
|
+
(options.log ?? console.error)(`prospectus_sign_in_failed ${JSON.stringify(entry)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function signInFailedRedirect(request: NextRequest, options: ProspectusSignInRouteOptions, returnPath: string | null, code: string): NextResponse {
|
|
114
|
+
const target = new URL(returnPath ?? "/", siteOriginFor(request, options));
|
|
115
|
+
target.searchParams.set("sign_in_error", /^[a-z0-9_]{1,60}$/.test(code) ? code : "sign_in_failed");
|
|
116
|
+
return NextResponse.redirect(target, { status: 303, headers: NO_STORE_HEADERS });
|
|
83
117
|
}
|
|
84
118
|
|
|
85
|
-
export
|
|
119
|
+
export { signedInProspectusSession } from "./prospectus-session";
|
|
120
|
+
|
|
121
|
+
/** `returnTo` from the query (a link) or, for the POST form the reader plugin's button submits, from the form body. */
|
|
122
|
+
async function requestedReturnPath(request: NextRequest): Promise<string | null> {
|
|
123
|
+
if (request.method !== "POST") return request.nextUrl.searchParams.get("returnTo");
|
|
124
|
+
try {
|
|
125
|
+
const value = (await request.formData()).get("returnTo");
|
|
126
|
+
return typeof value === "string" ? value : null;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** GET (a link) or POST (the reader plugin's "Sign in" button, so people are never sent to an /api URL) starts the Google sign-in with a 303 to Google. */
|
|
133
|
+
export function handleProspectusSignInStart(request: NextRequest, options: ProspectusSignInRouteOptions = {}): Promise<NextResponse> {
|
|
134
|
+
return failingClosedWithoutSiteOrigin(() => handleProspectusSignInStartRoute(request, options));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function handleProspectusSignInStartRoute(request: NextRequest, options: ProspectusSignInRouteOptions): Promise<NextResponse> {
|
|
86
138
|
const runtime = await signInRuntimeFor(request, options);
|
|
87
139
|
if (!runtime) return signInUnavailableResponse();
|
|
88
140
|
const { authorizationUrl, pendingSignIn } = await beginProspectusGoogleSignIn(runtime.accounts, runtime.cognitoClient);
|
|
89
141
|
const pendingSignInCookie: PendingProspectusSignInCookie = {
|
|
90
142
|
...pendingSignIn,
|
|
91
|
-
returnPath: sameSiteReturnPath(request
|
|
143
|
+
returnPath: sameSiteReturnPath(await requestedReturnPath(request)),
|
|
92
144
|
};
|
|
93
145
|
const response = NextResponse.redirect(authorizationUrl, { status: 303, headers: NO_STORE_HEADERS });
|
|
94
146
|
response.cookies.set(
|
|
@@ -99,7 +151,11 @@ export async function handleProspectusSignInStart(request: NextRequest, options:
|
|
|
99
151
|
return response;
|
|
100
152
|
}
|
|
101
153
|
|
|
102
|
-
export
|
|
154
|
+
export function handleProspectusSignInCallback(request: NextRequest, options: ProspectusSignInRouteOptions = {}): Promise<NextResponse> {
|
|
155
|
+
return failingClosedWithoutSiteOrigin(() => handleProspectusSignInCallbackRoute(request, options));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function handleProspectusSignInCallbackRoute(request: NextRequest, options: ProspectusSignInRouteOptions): Promise<NextResponse> {
|
|
103
159
|
const runtime = await signInRuntimeFor(request, options);
|
|
104
160
|
if (!runtime) return signInUnavailableResponse();
|
|
105
161
|
const nowInSeconds = currentTimeInSeconds(options);
|
|
@@ -121,12 +177,16 @@ async function completeCallback(
|
|
|
121
177
|
options: ProspectusSignInRouteOptions,
|
|
122
178
|
nowInSeconds: number,
|
|
123
179
|
): Promise<NextResponse> {
|
|
124
|
-
|
|
180
|
+
const failed = (code: string, branch: SignInFailureBranch, detail: { httpStatus?: number; prospectusError?: string; prospectusMessage?: string; errorName?: string } = {}) => {
|
|
181
|
+
logSignInFailure(options, { code, branch, ...detail });
|
|
182
|
+
return signInFailedRedirect(request, options, pendingSignIn?.returnPath ?? null, code);
|
|
183
|
+
};
|
|
184
|
+
if (!pendingSignIn) return failed("sign_in_expired", "pending_cookie");
|
|
125
185
|
const searchParams = request.nextUrl.searchParams;
|
|
126
|
-
if (searchParams.get("error")) return
|
|
186
|
+
if (searchParams.get("error")) return failed("sign_in_declined", "provider_error");
|
|
127
187
|
const authorizationCode = searchParams.get("code");
|
|
128
188
|
const returnedState = searchParams.get("state");
|
|
129
|
-
if (!authorizationCode || !returnedState) return
|
|
189
|
+
if (!authorizationCode || !returnedState) return failed("sign_in_incomplete", "missing_parameters");
|
|
130
190
|
try {
|
|
131
191
|
const signedIn = await completeProspectusGoogleSignIn({
|
|
132
192
|
accounts: runtime.accounts,
|
|
@@ -141,8 +201,9 @@ async function completeCallback(
|
|
|
141
201
|
personId: signedIn.person.id,
|
|
142
202
|
accessToken: signedIn.accessToken,
|
|
143
203
|
accessTokenExpiresAt: signedIn.accessTokenExpiresAt,
|
|
204
|
+
...(signedIn.person.siteSubject ? { siteSubject: signedIn.person.siteSubject } : {}),
|
|
144
205
|
};
|
|
145
|
-
const response = NextResponse.redirect(new URL(pendingSignIn.returnPath, request
|
|
206
|
+
const response = NextResponse.redirect(new URL(pendingSignIn.returnPath, siteOriginFor(request, options)), { status: 303, headers: NO_STORE_HEADERS });
|
|
146
207
|
response.cookies.set(
|
|
147
208
|
PROSPECTUS_SESSION_COOKIE,
|
|
148
209
|
sealProspectusCookie(runtime.sealingSecret, PROSPECTUS_SESSION_COOKIE, session, session.accessTokenExpiresAt),
|
|
@@ -150,13 +211,16 @@ async function completeCallback(
|
|
|
150
211
|
);
|
|
151
212
|
return response;
|
|
152
213
|
} catch (error) {
|
|
153
|
-
if (error instanceof ProspectusAccountsError) return
|
|
154
|
-
|
|
155
|
-
return signInFailedResponse(401, "sign_in_failed", "Sign-in could not be completed.");
|
|
214
|
+
if (error instanceof ProspectusAccountsError) return failed(error.code, "completion", { httpStatus: error.status, prospectusError: error.code, prospectusMessage: error.message.slice(0, 300) });
|
|
215
|
+
return failed("sign_in_failed", "unexpected", { errorName: error instanceof Error ? error.name : "error" });
|
|
156
216
|
}
|
|
157
217
|
}
|
|
158
218
|
|
|
159
|
-
export
|
|
219
|
+
export function handleProspectusSessionRead(request: NextRequest, options: ProspectusSignInRouteOptions = {}): Promise<NextResponse> {
|
|
220
|
+
return failingClosedWithoutSiteOrigin(() => handleProspectusSessionReadRoute(request, options));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function handleProspectusSessionReadRoute(request: NextRequest, options: ProspectusSignInRouteOptions): Promise<NextResponse> {
|
|
160
224
|
const runtime = await signInRuntimeFor(request, options);
|
|
161
225
|
if (!runtime) return signInUnavailableResponse();
|
|
162
226
|
const session = signedInProspectusSession(request, runtime.sealingSecret, currentTimeInSeconds(options));
|
|
@@ -169,11 +233,10 @@ export async function handleProspectusSessionRead(request: NextRequest, options:
|
|
|
169
233
|
return response;
|
|
170
234
|
}
|
|
171
235
|
const publishingAccounts = publishingAccountsForSite(directory.accounts, siteProductFor(options));
|
|
236
|
+
// Only what the UI reads: no person id, siteSubject or account list ever goes to the browser.
|
|
172
237
|
return NextResponse.json(
|
|
173
238
|
{
|
|
174
239
|
signedIn: true,
|
|
175
|
-
person: directory.person,
|
|
176
|
-
accounts: directory.accounts,
|
|
177
240
|
cyclotronLabAccess: directory.accounts.some((account) =>
|
|
178
241
|
account.products.some((product) => product.product === CYCLOTRON_LAB_PRODUCT && product.status === "active"),
|
|
179
242
|
),
|
|
@@ -196,9 +259,13 @@ export async function handleProspectusSessionRead(request: NextRequest, options:
|
|
|
196
259
|
}
|
|
197
260
|
}
|
|
198
261
|
|
|
199
|
-
export
|
|
262
|
+
export function handleProspectusSignOut(request: NextRequest, options: ProspectusSignInRouteOptions = {}): Promise<NextResponse> {
|
|
263
|
+
return failingClosedWithoutSiteOrigin(() => handleProspectusSignOutRoute(request, options));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function handleProspectusSignOutRoute(request: NextRequest, options: ProspectusSignInRouteOptions): Promise<NextResponse> {
|
|
200
267
|
const runtime = await signInRuntimeFor(request, options);
|
|
201
|
-
const signedOutDestination = new URL("/", request
|
|
268
|
+
const signedOutDestination = new URL("/", siteOriginFor(request, options));
|
|
202
269
|
const destination = runtime
|
|
203
270
|
? new URL(
|
|
204
271
|
`/logout?${new URLSearchParams({ client_id: runtime.cognitoClient.clientId, logout_uri: signedOutDestination.toString() })}`,
|
|
@@ -250,10 +317,14 @@ export async function currentProspectusPublishingSelection(
|
|
|
250
317
|
}
|
|
251
318
|
}
|
|
252
319
|
|
|
253
|
-
export
|
|
320
|
+
export function handleProspectusAccountSelection(request: NextRequest, options: ProspectusSignInRouteOptions = {}): Promise<NextResponse> {
|
|
321
|
+
return failingClosedWithoutSiteOrigin(() => handleProspectusAccountSelectionRoute(request, options));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function handleProspectusAccountSelectionRoute(request: NextRequest, options: ProspectusSignInRouteOptions): Promise<NextResponse> {
|
|
254
325
|
const runtime = await signInRuntimeFor(request, options);
|
|
255
326
|
if (!runtime) return signInUnavailableResponse();
|
|
256
|
-
if (request.headers.get("origin") !== request
|
|
327
|
+
if (request.headers.get("origin") !== siteOriginFor(request, options)) {
|
|
257
328
|
return signInFailedResponse(403, "cross_origin_selection", "Choose an account from this site.");
|
|
258
329
|
}
|
|
259
330
|
const nowInSeconds = currentTimeInSeconds(options);
|
|
@@ -287,10 +358,14 @@ const MAXIMUM_PUBLISHING_ACCOUNT_NAME_LENGTH = 150;
|
|
|
287
358
|
const DEFAULT_PUBLISHING_ACCOUNT_NAME = "Publishing account";
|
|
288
359
|
const IDEMPOTENCY_KEY_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
289
360
|
|
|
290
|
-
export
|
|
361
|
+
export function handleProspectusPublishingAccountCreation(request: NextRequest, options: ProspectusSignInRouteOptions = {}): Promise<NextResponse> {
|
|
362
|
+
return failingClosedWithoutSiteOrigin(() => handleProspectusPublishingAccountCreationRoute(request, options));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function handleProspectusPublishingAccountCreationRoute(request: NextRequest, options: ProspectusSignInRouteOptions): Promise<NextResponse> {
|
|
291
366
|
const runtime = await signInRuntimeFor(request, options);
|
|
292
367
|
if (!runtime) return signInUnavailableResponse();
|
|
293
|
-
if (request.headers.get("origin") !== request
|
|
368
|
+
if (request.headers.get("origin") !== siteOriginFor(request, options)) {
|
|
294
369
|
return signInFailedResponse(403, "cross_origin_creation", "Create an account from this site.");
|
|
295
370
|
}
|
|
296
371
|
const nowInSeconds = currentTimeInSeconds(options);
|