@anthusai/papyrus 1.0.0-next.9 → 1.1.0
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/prospectus-google-client.js +66 -0
- package/amplify/auth/prospectus-google-client.ts +71 -0
- package/amplify/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +23 -5
- package/amplify/auth/resource.ts +31 -5
- package/amplify/data/resource.js +42 -80
- package/amplify/data/resource.ts +42 -80
- package/amplify/data/schema.js +45 -62
- package/amplify/data/schema.ts +47 -62
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +143 -1
- package/amplify/functions/content-actions/requirements.txt +2 -1
- package/amplify/functions/content-actions/resource.js +6 -1
- package/amplify/functions/content-actions/resource.ts +9 -2
- package/amplify/functions/email-submission-processor/handler.py +3 -1
- package/amplify/functions/renditions/handler.py +120 -0
- package/amplify/functions/renditions/requirements.txt +4 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
- package/amplify/functions/shared/email-submission.ts +109 -1
- package/amplify/functions/shared/python-bundle.js +42 -2
- package/amplify/functions/shared/python-bundle.ts +47 -2
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/plugins/registry.js +19 -0
- package/amplify/plugins/registry.ts +23 -0
- package/amplify/plugins/signup/function/handler.py +200 -0
- package/amplify/plugins/signup/function/resource.js +27 -0
- package/amplify/plugins/signup/function/resource.ts +35 -0
- package/amplify/plugins/signup/papyrus.plugin.js +59 -0
- package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
- package/amplify/plugins/signup/schema.js +40 -0
- package/amplify/plugins/signup/schema.ts +41 -0
- package/amplify/plugins/signup/waf.js +48 -0
- package/amplify/plugins/signup/waf.ts +51 -0
- package/amplify/plugins/types.js +1 -0
- package/amplify/plugins/types.ts +39 -0
- package/amplify/plugins/wire.js +24 -0
- package/amplify/plugins/wire.ts +26 -0
- package/amplify/publication/default-publication.js +18 -0
- package/amplify/publication/default-publication.ts +20 -0
- package/amplify/publication/invalidation-alarm.js +26 -0
- package/amplify/publication/invalidation-alarm.ts +30 -0
- package/amplify/publication/redirect-and-index.js +37 -0
- package/amplify/publication/renditions-function.js +74 -0
- package/amplify/publication/renditions-function.ts +112 -0
- package/amplify/publication/renditions-settings.js +51 -0
- package/amplify/publication/renditions-settings.ts +58 -0
- package/amplify/publication/reproject-on-deploy.js +73 -0
- package/amplify/publication/reproject-on-deploy.ts +78 -0
- package/amplify/publication/s3-projection-settings.js +67 -0
- package/amplify/publication/s3-projection-settings.ts +101 -0
- package/amplify/publication/s3-projection.js +151 -0
- package/amplify/publication/s3-projection.ts +201 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/revalidate-invalidation-settings.js +30 -0
- package/amplify/revalidate-invalidation-settings.ts +35 -0
- package/amplify/revalidate-secret-policy.js +13 -0
- package/amplify/revalidate-secret-policy.ts +19 -0
- package/amplify/site-backend-config.js +65 -1
- package/amplify/site-backend-config.ts +153 -2
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +150 -176
- package/amplify/site-backend.ts +189 -201
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
- package/app/[year]/[month]/[day]/page.tsx +1 -1
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
- package/app/api/archive/editions/route.ts +1 -1
- package/app/api/auth/prospectus/callback/route.ts +9 -0
- package/app/api/auth/prospectus/selection/route.ts +9 -0
- package/app/api/auth/prospectus/session/route.ts +9 -0
- package/app/api/auth/prospectus/sign-in/route.ts +9 -0
- package/app/api/auth/prospectus/sign-out/route.ts +9 -0
- package/app/api/cyclotron/priming-proof/route.ts +11 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +15 -7
- package/app/archive/page.tsx +1 -1
- package/app/articles/[slug]/page.tsx +7 -5
- package/app/blog-rhythm.css +278 -0
- package/app/cyclotron/page.tsx +13 -0
- package/app/globals.css +2 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
- package/app/newsroom/[section]/page.tsx +6 -3
- package/app/newsroom/layout.tsx +4 -9
- package/app/newsroom/settings/page.tsx +6 -0
- package/app/newsroom/work/page.tsx +10 -0
- package/app/page.tsx +22 -8
- package/app/robots.ts +2 -5
- package/app/tailwind.css +1 -6
- package/components/amplify-client-provider.tsx +5 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +152 -5
- package/components/blog-page-background.tsx +23 -6
- package/components/news-desk-page.tsx +4 -15
- package/components/news-desk-taxonomy-client.ts +90 -3
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +2 -1
- package/components/newsroom-board-view.tsx +99 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-curation-provider.tsx +17 -0
- package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
- package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
- package/components/newsroom-newsprint-board.css +48 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +7 -1
- package/components/newsroom-organizer-shell.tsx +130 -0
- package/components/newsroom-organizer.css +63 -0
- package/components/newsroom-references-view.tsx +92 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/priming-proof-page.tsx +94 -0
- package/components/publication-cyclotron-panel.tsx +57 -0
- package/components/reader-auth-control.tsx +8 -5
- package/components/reference-curation-controls.tsx +77 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-priming.tsx +18 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/signup-entries-panel.tsx +94 -0
- package/components/topic-steering-workspace.tsx +78 -8
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-near-viewport.ts +30 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +166 -27
- package/infra/amplify-app-shell.ts +180 -31
- package/infra/build-specs.js +23 -7
- package/infra/build-specs.ts +27 -10
- package/infra/site-config.js +248 -36
- package/infra/site-config.ts +313 -51
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/amplify-server-runtime.ts +36 -1
- package/lib/approved-priming-proof-reports.ts +6 -0
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +43 -93
- package/lib/content-item-lifecycle.ts +55 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-types.ts +4 -0
- package/lib/curation-client.ts +61 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +56 -22
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-board.ts +99 -0
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-nav.ts +3 -1
- package/lib/newsroom-organizer-nav.ts +37 -0
- package/lib/newsroom-section-routing.ts +20 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/plugin-settings.ts +11 -0
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/priming-proof-report.ts +90 -0
- package/lib/prospectus-publication-access.ts +53 -0
- package/lib/prospectus-session.ts +196 -0
- package/lib/prospectus-sign-in-routes.ts +284 -0
- package/lib/prospectus-site-identity.ts +303 -0
- package/lib/publication-cyclotron.ts +74 -0
- package/lib/rate-limit-retry.ts +55 -0
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-cache-lifetime.ts +5 -0
- package/lib/reader-static-params.ts +85 -4
- package/lib/reader-storage-url.ts +9 -12
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-prewarm.ts +53 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/shared-credentials-provider.ts +61 -0
- package/lib/signup-csv.ts +24 -0
- package/lib/site-brand.ts +59 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +21 -5
- package/lib/test-editor-override.ts +4 -0
- package/lib/video-loading.ts +22 -0
- package/lib/video-script.ts +14 -0
- package/middleware.ts +11 -19
- package/package.json +73 -8
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +123 -12
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.d.mts +9 -0
- package/src/with-papyrus.mjs +48 -3
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
- package/components/ai-elements/message.tsx +0 -360
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CfnOutput } from "aws-cdk-lib";
|
|
2
|
+
import { CfnUserPoolClient, CfnUserPoolIdentityProvider } from "aws-cdk-lib/aws-cognito";
|
|
3
|
+
/**
|
|
4
|
+
* The Google-only app client a Papyrus site registers with Prospectus
|
|
5
|
+
* (Prospectus docs/accounts/GOOGLE_IDENTITY.md, policy google-only-unlinked-v1).
|
|
6
|
+
* It lives on the site's existing user pool next to the email + Google web
|
|
7
|
+
* client, but admits only the built-in Google provider through code + PKCE from
|
|
8
|
+
* the site server, with every native, SRP, custom and client-credentials path
|
|
9
|
+
* left off. Sessions last one access token: the server keeps no refresh token.
|
|
10
|
+
*/
|
|
11
|
+
export const PROSPECTUS_SIGN_IN_CALLBACK_PATH = "/api/auth/prospectus/callback";
|
|
12
|
+
export const PROSPECTUS_SIGN_IN_TOKEN_VALIDITY_MINUTES = 60;
|
|
13
|
+
export function prospectusSignInCallbackUrls(redirectUrls) {
|
|
14
|
+
const origins = [];
|
|
15
|
+
for (const redirectUrl of redirectUrls) {
|
|
16
|
+
try {
|
|
17
|
+
const origin = new URL(redirectUrl).origin;
|
|
18
|
+
if (origin !== "null" && !origins.includes(origin))
|
|
19
|
+
origins.push(origin);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return origins.map((origin) => `${origin}${PROSPECTUS_SIGN_IN_CALLBACK_PATH}`);
|
|
26
|
+
}
|
|
27
|
+
export function prospectusSignInLogoutUrls(redirectUrls) {
|
|
28
|
+
return prospectusSignInCallbackUrls(redirectUrls).map((callbackUrl) => `${new URL(callbackUrl).origin}/`);
|
|
29
|
+
}
|
|
30
|
+
export function addProspectusGoogleOnlyClient(authStack, userPool, redirectUrls) {
|
|
31
|
+
const callbackUrls = prospectusSignInCallbackUrls(redirectUrls);
|
|
32
|
+
if (callbackUrls.length === 0)
|
|
33
|
+
throw new Error("The Prospectus Google-only client needs at least one site origin.");
|
|
34
|
+
const googleOnlyClient = new CfnUserPoolClient(authStack, "ProspectusGoogleOnlyClient", {
|
|
35
|
+
userPoolId: userPool.userPoolId,
|
|
36
|
+
clientName: "prospectus-google-only",
|
|
37
|
+
generateSecret: false,
|
|
38
|
+
supportedIdentityProviders: ["Google"],
|
|
39
|
+
allowedOAuthFlowsUserPoolClient: true,
|
|
40
|
+
allowedOAuthFlows: ["code"],
|
|
41
|
+
allowedOAuthScopes: ["openid", "email", "profile"],
|
|
42
|
+
callbackUrLs: callbackUrls,
|
|
43
|
+
logoutUrLs: prospectusSignInLogoutUrls(redirectUrls),
|
|
44
|
+
explicitAuthFlows: ["ALLOW_REFRESH_TOKEN_AUTH"],
|
|
45
|
+
enableTokenRevocation: true,
|
|
46
|
+
preventUserExistenceErrors: "ENABLED",
|
|
47
|
+
accessTokenValidity: PROSPECTUS_SIGN_IN_TOKEN_VALIDITY_MINUTES,
|
|
48
|
+
idTokenValidity: PROSPECTUS_SIGN_IN_TOKEN_VALIDITY_MINUTES,
|
|
49
|
+
refreshTokenValidity: PROSPECTUS_SIGN_IN_TOKEN_VALIDITY_MINUTES,
|
|
50
|
+
tokenValidityUnits: { accessToken: "minutes", idToken: "minutes", refreshToken: "minutes" },
|
|
51
|
+
});
|
|
52
|
+
const googleIdentityProviders = authStack.node
|
|
53
|
+
.findAll()
|
|
54
|
+
.filter((construct) => construct instanceof CfnUserPoolIdentityProvider && construct.providerName === "Google");
|
|
55
|
+
for (const googleIdentityProvider of googleIdentityProviders)
|
|
56
|
+
googleOnlyClient.addDependency(googleIdentityProvider);
|
|
57
|
+
new CfnOutput(authStack, "ProspectusGoogleOnlyClientId", {
|
|
58
|
+
value: googleOnlyClient.ref,
|
|
59
|
+
description: "clientId for this site's Prospectus registration (accounts/papyrus-sites.json)",
|
|
60
|
+
});
|
|
61
|
+
new CfnOutput(authStack, "ProspectusGoogleOnlyIssuer", {
|
|
62
|
+
value: `https://cognito-idp.${authStack.region}.amazonaws.com/${userPool.userPoolId}`,
|
|
63
|
+
description: "issuer for this site's Prospectus registration (accounts/papyrus-sites.json)",
|
|
64
|
+
});
|
|
65
|
+
return googleOnlyClient;
|
|
66
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CfnOutput, type Stack } from "aws-cdk-lib";
|
|
2
|
+
import { CfnUserPoolClient, CfnUserPoolIdentityProvider, type IUserPool } from "aws-cdk-lib/aws-cognito";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Google-only app client a Papyrus site registers with Prospectus
|
|
6
|
+
* (Prospectus docs/accounts/GOOGLE_IDENTITY.md, policy google-only-unlinked-v1).
|
|
7
|
+
* It lives on the site's existing user pool next to the email + Google web
|
|
8
|
+
* client, but admits only the built-in Google provider through code + PKCE from
|
|
9
|
+
* the site server, with every native, SRP, custom and client-credentials path
|
|
10
|
+
* left off. Sessions last one access token: the server keeps no refresh token.
|
|
11
|
+
*/
|
|
12
|
+
export const PROSPECTUS_SIGN_IN_CALLBACK_PATH = "/api/auth/prospectus/callback";
|
|
13
|
+
export const PROSPECTUS_SIGN_IN_TOKEN_VALIDITY_MINUTES = 60;
|
|
14
|
+
|
|
15
|
+
export function prospectusSignInCallbackUrls(redirectUrls: readonly string[]): string[] {
|
|
16
|
+
const origins: string[] = [];
|
|
17
|
+
for (const redirectUrl of redirectUrls) {
|
|
18
|
+
try {
|
|
19
|
+
const origin = new URL(redirectUrl).origin;
|
|
20
|
+
if (origin !== "null" && !origins.includes(origin)) origins.push(origin);
|
|
21
|
+
} catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return origins.map((origin) => `${origin}${PROSPECTUS_SIGN_IN_CALLBACK_PATH}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function prospectusSignInLogoutUrls(redirectUrls: readonly string[]): string[] {
|
|
29
|
+
return prospectusSignInCallbackUrls(redirectUrls).map((callbackUrl) => `${new URL(callbackUrl).origin}/`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function addProspectusGoogleOnlyClient(
|
|
33
|
+
authStack: Stack,
|
|
34
|
+
userPool: IUserPool,
|
|
35
|
+
redirectUrls: readonly string[],
|
|
36
|
+
): CfnUserPoolClient {
|
|
37
|
+
const callbackUrls = prospectusSignInCallbackUrls(redirectUrls);
|
|
38
|
+
if (callbackUrls.length === 0) throw new Error("The Prospectus Google-only client needs at least one site origin.");
|
|
39
|
+
const googleOnlyClient = new CfnUserPoolClient(authStack, "ProspectusGoogleOnlyClient", {
|
|
40
|
+
userPoolId: userPool.userPoolId,
|
|
41
|
+
clientName: "prospectus-google-only",
|
|
42
|
+
generateSecret: false,
|
|
43
|
+
supportedIdentityProviders: ["Google"],
|
|
44
|
+
allowedOAuthFlowsUserPoolClient: true,
|
|
45
|
+
allowedOAuthFlows: ["code"],
|
|
46
|
+
allowedOAuthScopes: ["openid", "email", "profile"],
|
|
47
|
+
callbackUrLs: callbackUrls,
|
|
48
|
+
logoutUrLs: prospectusSignInLogoutUrls(redirectUrls),
|
|
49
|
+
explicitAuthFlows: ["ALLOW_REFRESH_TOKEN_AUTH"],
|
|
50
|
+
enableTokenRevocation: true,
|
|
51
|
+
preventUserExistenceErrors: "ENABLED",
|
|
52
|
+
accessTokenValidity: PROSPECTUS_SIGN_IN_TOKEN_VALIDITY_MINUTES,
|
|
53
|
+
idTokenValidity: PROSPECTUS_SIGN_IN_TOKEN_VALIDITY_MINUTES,
|
|
54
|
+
refreshTokenValidity: PROSPECTUS_SIGN_IN_TOKEN_VALIDITY_MINUTES,
|
|
55
|
+
tokenValidityUnits: { accessToken: "minutes", idToken: "minutes", refreshToken: "minutes" },
|
|
56
|
+
});
|
|
57
|
+
const googleIdentityProviders = authStack.node
|
|
58
|
+
.findAll()
|
|
59
|
+
.filter((construct): construct is CfnUserPoolIdentityProvider => construct instanceof CfnUserPoolIdentityProvider && construct.providerName === "Google");
|
|
60
|
+
for (const googleIdentityProvider of googleIdentityProviders) googleOnlyClient.addDependency(googleIdentityProvider);
|
|
61
|
+
|
|
62
|
+
new CfnOutput(authStack, "ProspectusGoogleOnlyClientId", {
|
|
63
|
+
value: googleOnlyClient.ref,
|
|
64
|
+
description: "clientId for this site's Prospectus registration (accounts/papyrus-sites.json)",
|
|
65
|
+
});
|
|
66
|
+
new CfnOutput(authStack, "ProspectusGoogleOnlyIssuer", {
|
|
67
|
+
value: `https://cognito-idp.${authStack.region}.amazonaws.com/${userPool.userPoolId}`,
|
|
68
|
+
description: "issuer for this site's Prospectus registration (accounts/papyrus-sites.json)",
|
|
69
|
+
});
|
|
70
|
+
return googleOnlyClient;
|
|
71
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const DEFAULT_NEWSROOM_BASE_PATH = "/newsroom";
|
|
2
|
+
function parseRedirectUrl(value) {
|
|
3
|
+
try {
|
|
4
|
+
const parsed = new URL(value);
|
|
5
|
+
return { original: value, origin: parsed.origin, path: parsed.pathname.replace(/\/+$/, "") };
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Expands every origin in an OAuth redirect list to both the newsroom return
|
|
13
|
+
* path and the site root, newsroom first. Amplify signs in with the first
|
|
14
|
+
* entry that matches the current origin, so a list that names only
|
|
15
|
+
* `https://host/` would land a signed-in editor on the public reader.
|
|
16
|
+
*
|
|
17
|
+
* With `newsroomBasePath` of `""` (a CMS-only host that mounts the newsroom at
|
|
18
|
+
* `/`), the root already is the newsroom and nothing is added. Explicit
|
|
19
|
+
* entries are kept; unparseable entries pass through unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export function expandNewsroomRedirectUrls(redirectUrls, newsroomBasePath = DEFAULT_NEWSROOM_BASE_PATH) {
|
|
22
|
+
const newsroomPath = newsroomBasePath.replace(/\/+$/, "");
|
|
23
|
+
const originOrder = [];
|
|
24
|
+
const explicitByOrigin = new Map();
|
|
25
|
+
const passthrough = [];
|
|
26
|
+
for (const value of redirectUrls) {
|
|
27
|
+
const parsed = parseRedirectUrl(value);
|
|
28
|
+
if (!parsed) {
|
|
29
|
+
passthrough.push(value);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!explicitByOrigin.has(parsed.origin)) {
|
|
33
|
+
originOrder.push(parsed.origin);
|
|
34
|
+
explicitByOrigin.set(parsed.origin, []);
|
|
35
|
+
}
|
|
36
|
+
explicitByOrigin.get(parsed.origin)?.push(value);
|
|
37
|
+
}
|
|
38
|
+
const expanded = [];
|
|
39
|
+
for (const origin of originOrder) {
|
|
40
|
+
const explicit = explicitByOrigin.get(origin) ?? [];
|
|
41
|
+
if (newsroomPath === "") {
|
|
42
|
+
expanded.push(...explicit);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const newsroomEntries = explicit.filter((value) => parseRedirectUrl(value)?.path === newsroomPath);
|
|
46
|
+
const otherEntries = explicit.filter((value) => {
|
|
47
|
+
const path = parseRedirectUrl(value)?.path;
|
|
48
|
+
return path !== newsroomPath && path !== "";
|
|
49
|
+
});
|
|
50
|
+
expanded.push(...(newsroomEntries.length > 0 ? newsroomEntries : [`${origin}${newsroomPath}`]), `${origin}/`, ...otherEntries);
|
|
51
|
+
}
|
|
52
|
+
expanded.push(...passthrough);
|
|
53
|
+
return [...new Set(expanded)];
|
|
54
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const DEFAULT_NEWSROOM_BASE_PATH = "/newsroom";
|
|
2
|
+
|
|
3
|
+
type ParsedRedirectUrl = { original: string; origin: string; path: string } | null;
|
|
4
|
+
|
|
5
|
+
function parseRedirectUrl(value: string): ParsedRedirectUrl {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = new URL(value);
|
|
8
|
+
return { original: value, origin: parsed.origin, path: parsed.pathname.replace(/\/+$/, "") };
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Expands every origin in an OAuth redirect list to both the newsroom return
|
|
16
|
+
* path and the site root, newsroom first. Amplify signs in with the first
|
|
17
|
+
* entry that matches the current origin, so a list that names only
|
|
18
|
+
* `https://host/` would land a signed-in editor on the public reader.
|
|
19
|
+
*
|
|
20
|
+
* With `newsroomBasePath` of `""` (a CMS-only host that mounts the newsroom at
|
|
21
|
+
* `/`), the root already is the newsroom and nothing is added. Explicit
|
|
22
|
+
* entries are kept; unparseable entries pass through unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export function expandNewsroomRedirectUrls(
|
|
25
|
+
redirectUrls: readonly string[],
|
|
26
|
+
newsroomBasePath: string = DEFAULT_NEWSROOM_BASE_PATH,
|
|
27
|
+
): string[] {
|
|
28
|
+
const newsroomPath = newsroomBasePath.replace(/\/+$/, "");
|
|
29
|
+
const originOrder: string[] = [];
|
|
30
|
+
const explicitByOrigin = new Map<string, string[]>();
|
|
31
|
+
const passthrough: string[] = [];
|
|
32
|
+
|
|
33
|
+
for (const value of redirectUrls) {
|
|
34
|
+
const parsed = parseRedirectUrl(value);
|
|
35
|
+
if (!parsed) {
|
|
36
|
+
passthrough.push(value);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!explicitByOrigin.has(parsed.origin)) {
|
|
40
|
+
originOrder.push(parsed.origin);
|
|
41
|
+
explicitByOrigin.set(parsed.origin, []);
|
|
42
|
+
}
|
|
43
|
+
explicitByOrigin.get(parsed.origin)?.push(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const expanded: string[] = [];
|
|
47
|
+
for (const origin of originOrder) {
|
|
48
|
+
const explicit = explicitByOrigin.get(origin) ?? [];
|
|
49
|
+
if (newsroomPath === "") {
|
|
50
|
+
expanded.push(...explicit);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const newsroomEntries = explicit.filter((value) => parseRedirectUrl(value)?.path === newsroomPath);
|
|
54
|
+
const otherEntries = explicit.filter((value) => {
|
|
55
|
+
const path = parseRedirectUrl(value)?.path;
|
|
56
|
+
return path !== newsroomPath && path !== "";
|
|
57
|
+
});
|
|
58
|
+
expanded.push(
|
|
59
|
+
...(newsroomEntries.length > 0 ? newsroomEntries : [`${origin}${newsroomPath}`]),
|
|
60
|
+
`${origin}/`,
|
|
61
|
+
...otherEntries,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
expanded.push(...passthrough);
|
|
65
|
+
return [...new Set(expanded)];
|
|
66
|
+
}
|
package/amplify/auth/resource.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { defineAuth, secret } from "@aws-amplify/backend";
|
|
2
2
|
import { manageUserRole } from "../functions/manage-user-role/resource.js";
|
|
3
|
+
import { CfnUserPoolDomain } from "aws-cdk-lib/aws-cognito";
|
|
4
|
+
import { expandNewsroomRedirectUrls } from "./redirect-urls.js";
|
|
3
5
|
// Default OAuth redirect URLs for the p.apyr.us app. Other publications pass
|
|
4
6
|
// `auth.redirectUrls` to `defineSiteBackend` (or, for Papyrus's own backend,
|
|
5
7
|
// the PAPYRUS_OAUTH_REDIRECT_URLS branch env var, comma-separated).
|
|
8
|
+
// `defineSiteAuth` adds the newsroom and root variants of every origin listed.
|
|
6
9
|
// See docs/google-oauth-setup.md.
|
|
7
10
|
const DEFAULT_AUTH_REDIRECT_URLS = [
|
|
8
11
|
"http://localhost:3001/",
|
|
@@ -25,15 +28,16 @@ export function authConfigFromEnv() {
|
|
|
25
28
|
return {
|
|
26
29
|
redirectUrls: resolveAuthRedirectUrls(),
|
|
27
30
|
disableGoogleOAuth: process.env.PAPYRUS_DISABLE_GOOGLE_OAUTH === "1",
|
|
28
|
-
// Stable Cognito hosted-UI domain prefix for Google OAuth redirect URIs.
|
|
29
|
-
// See docs/google-oauth-setup.md.
|
|
30
31
|
cognitoDomainPrefix: (process.env.PAPYRUS_COGNITO_DOMAIN_PREFIX ?? "").trim(),
|
|
32
|
+
applyCognitoDomainPrefix: process.env.PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX === "true",
|
|
31
33
|
};
|
|
32
34
|
}
|
|
35
|
+
export function siteAuthRedirectUrls(config) {
|
|
36
|
+
return config.redirectUrls ?? DEFAULT_AUTH_REDIRECT_URLS;
|
|
37
|
+
}
|
|
33
38
|
export function defineSiteAuth(config) {
|
|
34
|
-
const authRedirectUrls = config.
|
|
39
|
+
const authRedirectUrls = expandNewsroomRedirectUrls(siteAuthRedirectUrls(config), config.newsroomBasePath);
|
|
35
40
|
const disableGoogleOAuth = config.disableGoogleOAuth === true;
|
|
36
|
-
const cognitoDomainPrefix = (config.cognitoDomainPrefix ?? "").trim();
|
|
37
41
|
return defineAuth({
|
|
38
42
|
loginWith: {
|
|
39
43
|
email: true,
|
|
@@ -49,7 +53,6 @@ export function defineSiteAuth(config) {
|
|
|
49
53
|
scopes: ["EMAIL", "PROFILE", "OPENID"],
|
|
50
54
|
callbackUrls: authRedirectUrls,
|
|
51
55
|
logoutUrls: authRedirectUrls,
|
|
52
|
-
...(cognitoDomainPrefix ? { domainPrefix: cognitoDomainPrefix } : {}),
|
|
53
56
|
},
|
|
54
57
|
}),
|
|
55
58
|
},
|
|
@@ -59,3 +62,18 @@ export function defineSiteAuth(config) {
|
|
|
59
62
|
],
|
|
60
63
|
});
|
|
61
64
|
}
|
|
65
|
+
// Amplify Gen2's auth factory overwrites `externalProviders.domainPrefix` with a
|
|
66
|
+
// hash of the backend id, so the prefix cannot be passed through `defineAuth`.
|
|
67
|
+
// This sets the user pool domain on the synthesized resource instead. Cognito
|
|
68
|
+
// cannot rename a pool domain in place: CloudFormation replaces the domain,
|
|
69
|
+
// which changes the hosted-UI URL and the Google redirect URI. Callers must
|
|
70
|
+
// only use it when the site opted in (see docs/google-oauth-setup.md).
|
|
71
|
+
export function applyCognitoDomainPrefix(authStack, cognitoDomainPrefix) {
|
|
72
|
+
const domainResources = authStack.node
|
|
73
|
+
.findAll()
|
|
74
|
+
.filter((construct) => construct instanceof CfnUserPoolDomain);
|
|
75
|
+
if (domainResources.length !== 1) {
|
|
76
|
+
throw new Error(`Expected exactly one Cognito user pool domain in the auth stack to apply cognitoDomainPrefix, found ${domainResources.length}`);
|
|
77
|
+
}
|
|
78
|
+
domainResources[0].domain = cognitoDomainPrefix;
|
|
79
|
+
}
|
package/amplify/auth/resource.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { defineAuth, secret } from "@aws-amplify/backend";
|
|
2
2
|
import { manageUserRole } from "../functions/manage-user-role/resource";
|
|
3
|
+
import { Stack } from "aws-cdk-lib";
|
|
4
|
+
import { CfnUserPoolDomain } from "aws-cdk-lib/aws-cognito";
|
|
3
5
|
import type { PapyrusAuthConfig } from "../site-backend-config";
|
|
6
|
+
import { expandNewsroomRedirectUrls } from "./redirect-urls";
|
|
4
7
|
|
|
5
8
|
// Default OAuth redirect URLs for the p.apyr.us app. Other publications pass
|
|
6
9
|
// `auth.redirectUrls` to `defineSiteBackend` (or, for Papyrus's own backend,
|
|
7
10
|
// the PAPYRUS_OAUTH_REDIRECT_URLS branch env var, comma-separated).
|
|
11
|
+
// `defineSiteAuth` adds the newsroom and root variants of every origin listed.
|
|
8
12
|
// See docs/google-oauth-setup.md.
|
|
9
13
|
const DEFAULT_AUTH_REDIRECT_URLS = [
|
|
10
14
|
"http://localhost:3001/",
|
|
@@ -28,16 +32,21 @@ export function authConfigFromEnv(): PapyrusAuthConfig {
|
|
|
28
32
|
return {
|
|
29
33
|
redirectUrls: resolveAuthRedirectUrls(),
|
|
30
34
|
disableGoogleOAuth: process.env.PAPYRUS_DISABLE_GOOGLE_OAUTH === "1",
|
|
31
|
-
// Stable Cognito hosted-UI domain prefix for Google OAuth redirect URIs.
|
|
32
|
-
// See docs/google-oauth-setup.md.
|
|
33
35
|
cognitoDomainPrefix: (process.env.PAPYRUS_COGNITO_DOMAIN_PREFIX ?? "").trim(),
|
|
36
|
+
applyCognitoDomainPrefix: process.env.PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX === "true",
|
|
34
37
|
};
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
export function siteAuthRedirectUrls(config: PapyrusAuthConfig): string[] {
|
|
41
|
+
return config.redirectUrls ?? DEFAULT_AUTH_REDIRECT_URLS;
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
export function defineSiteAuth(config: PapyrusAuthConfig) {
|
|
38
|
-
const authRedirectUrls =
|
|
45
|
+
const authRedirectUrls = expandNewsroomRedirectUrls(
|
|
46
|
+
siteAuthRedirectUrls(config),
|
|
47
|
+
config.newsroomBasePath,
|
|
48
|
+
);
|
|
39
49
|
const disableGoogleOAuth = config.disableGoogleOAuth === true;
|
|
40
|
-
const cognitoDomainPrefix = (config.cognitoDomainPrefix ?? "").trim();
|
|
41
50
|
return defineAuth({
|
|
42
51
|
loginWith: {
|
|
43
52
|
email: true,
|
|
@@ -53,7 +62,6 @@ export function defineSiteAuth(config: PapyrusAuthConfig) {
|
|
|
53
62
|
scopes: ["EMAIL", "PROFILE", "OPENID"],
|
|
54
63
|
callbackUrls: authRedirectUrls,
|
|
55
64
|
logoutUrls: authRedirectUrls,
|
|
56
|
-
...(cognitoDomainPrefix ? { domainPrefix: cognitoDomainPrefix } : {}),
|
|
57
65
|
},
|
|
58
66
|
}),
|
|
59
67
|
},
|
|
@@ -63,3 +71,21 @@ export function defineSiteAuth(config: PapyrusAuthConfig) {
|
|
|
63
71
|
],
|
|
64
72
|
});
|
|
65
73
|
}
|
|
74
|
+
|
|
75
|
+
// Amplify Gen2's auth factory overwrites `externalProviders.domainPrefix` with a
|
|
76
|
+
// hash of the backend id, so the prefix cannot be passed through `defineAuth`.
|
|
77
|
+
// This sets the user pool domain on the synthesized resource instead. Cognito
|
|
78
|
+
// cannot rename a pool domain in place: CloudFormation replaces the domain,
|
|
79
|
+
// which changes the hosted-UI URL and the Google redirect URI. Callers must
|
|
80
|
+
// only use it when the site opted in (see docs/google-oauth-setup.md).
|
|
81
|
+
export function applyCognitoDomainPrefix(authStack: Stack, cognitoDomainPrefix: string): void {
|
|
82
|
+
const domainResources = authStack.node
|
|
83
|
+
.findAll()
|
|
84
|
+
.filter((construct): construct is CfnUserPoolDomain => construct instanceof CfnUserPoolDomain);
|
|
85
|
+
if (domainResources.length !== 1) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Expected exactly one Cognito user pool domain in the auth stack to apply cognitoDomainPrefix, found ${domainResources.length}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
domainResources[0].domain = cognitoDomainPrefix;
|
|
91
|
+
}
|