@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.
Files changed (222) hide show
  1. package/amplify/auth/prospectus-google-client.js +66 -0
  2. package/amplify/auth/prospectus-google-client.ts +71 -0
  3. package/amplify/auth/redirect-urls.js +54 -0
  4. package/amplify/auth/redirect-urls.ts +66 -0
  5. package/amplify/auth/resource.js +23 -5
  6. package/amplify/auth/resource.ts +31 -5
  7. package/amplify/data/resource.js +42 -80
  8. package/amplify/data/resource.ts +42 -80
  9. package/amplify/data/schema.js +45 -62
  10. package/amplify/data/schema.ts +47 -62
  11. package/amplify/functions/category-action/handler.ts +5 -3
  12. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  13. package/amplify/functions/console-chat-responder/resource.js +0 -7
  14. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  15. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  16. package/amplify/functions/content-actions/handler.py +143 -1
  17. package/amplify/functions/content-actions/requirements.txt +2 -1
  18. package/amplify/functions/content-actions/resource.js +6 -1
  19. package/amplify/functions/content-actions/resource.ts +9 -2
  20. package/amplify/functions/email-submission-processor/handler.py +3 -1
  21. package/amplify/functions/renditions/handler.py +120 -0
  22. package/amplify/functions/renditions/requirements.txt +4 -0
  23. package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
  24. package/amplify/functions/shared/email-submission.ts +109 -1
  25. package/amplify/functions/shared/python-bundle.js +42 -2
  26. package/amplify/functions/shared/python-bundle.ts +47 -2
  27. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  28. package/amplify/inbound-email/ses-intake.js +86 -0
  29. package/amplify/inbound-email/ses-intake.ts +110 -0
  30. package/amplify/plugins/registry.js +19 -0
  31. package/amplify/plugins/registry.ts +23 -0
  32. package/amplify/plugins/signup/function/handler.py +200 -0
  33. package/amplify/plugins/signup/function/resource.js +27 -0
  34. package/amplify/plugins/signup/function/resource.ts +35 -0
  35. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  36. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  37. package/amplify/plugins/signup/schema.js +40 -0
  38. package/amplify/plugins/signup/schema.ts +41 -0
  39. package/amplify/plugins/signup/waf.js +48 -0
  40. package/amplify/plugins/signup/waf.ts +51 -0
  41. package/amplify/plugins/types.js +1 -0
  42. package/amplify/plugins/types.ts +39 -0
  43. package/amplify/plugins/wire.js +24 -0
  44. package/amplify/plugins/wire.ts +26 -0
  45. package/amplify/publication/default-publication.js +18 -0
  46. package/amplify/publication/default-publication.ts +20 -0
  47. package/amplify/publication/invalidation-alarm.js +26 -0
  48. package/amplify/publication/invalidation-alarm.ts +30 -0
  49. package/amplify/publication/redirect-and-index.js +37 -0
  50. package/amplify/publication/renditions-function.js +74 -0
  51. package/amplify/publication/renditions-function.ts +112 -0
  52. package/amplify/publication/renditions-settings.js +51 -0
  53. package/amplify/publication/renditions-settings.ts +58 -0
  54. package/amplify/publication/reproject-on-deploy.js +73 -0
  55. package/amplify/publication/reproject-on-deploy.ts +78 -0
  56. package/amplify/publication/s3-projection-settings.js +67 -0
  57. package/amplify/publication/s3-projection-settings.ts +101 -0
  58. package/amplify/publication/s3-projection.js +151 -0
  59. package/amplify/publication/s3-projection.ts +201 -0
  60. package/amplify/resolver-waves.js +26 -0
  61. package/amplify/resolver-waves.ts +34 -0
  62. package/amplify/revalidate-invalidation-settings.js +30 -0
  63. package/amplify/revalidate-invalidation-settings.ts +35 -0
  64. package/amplify/revalidate-secret-policy.js +13 -0
  65. package/amplify/revalidate-secret-policy.ts +19 -0
  66. package/amplify/site-backend-config.js +65 -1
  67. package/amplify/site-backend-config.ts +153 -2
  68. package/amplify/site-backend-names.js +103 -0
  69. package/amplify/site-backend-names.ts +166 -0
  70. package/amplify/site-backend.js +150 -176
  71. package/amplify/site-backend.ts +189 -201
  72. package/amplify/storage-backups/construct.js +31 -0
  73. package/amplify/storage-backups/construct.ts +47 -0
  74. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  75. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  76. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  77. package/app/[year]/[month]/[day]/page.tsx +1 -1
  78. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  79. package/app/api/archive/editions/route.ts +1 -1
  80. package/app/api/auth/prospectus/callback/route.ts +9 -0
  81. package/app/api/auth/prospectus/selection/route.ts +9 -0
  82. package/app/api/auth/prospectus/session/route.ts +9 -0
  83. package/app/api/auth/prospectus/sign-in/route.ts +9 -0
  84. package/app/api/auth/prospectus/sign-out/route.ts +9 -0
  85. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  86. package/app/api/media/[...path]/route.ts +5 -16
  87. package/app/api/revalidate/route.ts +15 -7
  88. package/app/archive/page.tsx +1 -1
  89. package/app/articles/[slug]/page.tsx +7 -5
  90. package/app/blog-rhythm.css +278 -0
  91. package/app/cyclotron/page.tsx +13 -0
  92. package/app/globals.css +2 -0
  93. package/app/layout.tsx +16 -0
  94. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  95. package/app/newsroom/[section]/page.tsx +6 -3
  96. package/app/newsroom/layout.tsx +4 -9
  97. package/app/newsroom/settings/page.tsx +6 -0
  98. package/app/newsroom/work/page.tsx +10 -0
  99. package/app/page.tsx +22 -8
  100. package/app/robots.ts +2 -5
  101. package/app/tailwind.css +1 -6
  102. package/components/amplify-client-provider.tsx +5 -22
  103. package/components/archive-shell.tsx +2 -22
  104. package/components/article-video.tsx +152 -5
  105. package/components/blog-page-background.tsx +23 -6
  106. package/components/news-desk-page.tsx +4 -15
  107. package/components/news-desk-taxonomy-client.ts +90 -3
  108. package/components/newspaper.tsx +6 -2
  109. package/components/newsroom-access-gate.tsx +2 -1
  110. package/components/newsroom-board-view.tsx +99 -0
  111. package/components/newsroom-client-shell.tsx +12 -0
  112. package/components/newsroom-curation-provider.tsx +17 -0
  113. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  114. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  115. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  116. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  117. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  118. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  119. package/components/newsroom-newsprint-board.css +48 -0
  120. package/components/newsroom-ops-overview.tsx +28 -11
  121. package/components/newsroom-ops-shell.tsx +7 -1
  122. package/components/newsroom-organizer-shell.tsx +130 -0
  123. package/components/newsroom-organizer.css +63 -0
  124. package/components/newsroom-references-view.tsx +92 -8
  125. package/components/papyrus-console-shell.tsx +4 -2
  126. package/components/pictogram-figure.tsx +9 -2
  127. package/components/presentation-header.tsx +135 -0
  128. package/components/presentation-rhythm-hrule.tsx +17 -0
  129. package/components/priming-proof-page.tsx +94 -0
  130. package/components/publication-cyclotron-panel.tsx +57 -0
  131. package/components/reader-auth-control.tsx +8 -5
  132. package/components/reference-curation-controls.tsx +77 -0
  133. package/components/reference-cyclotron-status.tsx +109 -0
  134. package/components/reference-priming.tsx +18 -0
  135. package/components/reference-relevance-review.tsx +75 -0
  136. package/components/settings-page.tsx +2 -1
  137. package/components/signup-entries-panel.tsx +94 -0
  138. package/components/topic-steering-workspace.tsx +78 -8
  139. package/components/use-blog-header-obstacles.ts +81 -0
  140. package/components/use-brand-fonts-ready.ts +28 -0
  141. package/components/use-near-viewport.ts +30 -0
  142. package/components/use-rhythm-overlay.ts +29 -0
  143. package/infra/amplify-app-shell.js +166 -27
  144. package/infra/amplify-app-shell.ts +180 -31
  145. package/infra/build-specs.js +23 -7
  146. package/infra/build-specs.ts +27 -10
  147. package/infra/site-config.js +248 -36
  148. package/infra/site-config.ts +313 -51
  149. package/lib/amplify-outputs-path.ts +5 -1
  150. package/lib/amplify-server-runtime.ts +36 -1
  151. package/lib/approved-priming-proof-reports.ts +6 -0
  152. package/lib/blog-feature-solver.ts +459 -0
  153. package/lib/blog-rhythm.ts +134 -0
  154. package/lib/brand-fonts-watch.ts +43 -0
  155. package/lib/brand-fonts.ts +112 -0
  156. package/lib/cached-content-repository.ts +12 -0
  157. package/lib/category-repository.ts +43 -93
  158. package/lib/content-item-lifecycle.ts +55 -0
  159. package/lib/content-repository.ts +3 -0
  160. package/lib/content-types.ts +4 -0
  161. package/lib/curation-client.ts +61 -0
  162. package/lib/edition-routes.ts +31 -29
  163. package/lib/graphql-content-repository.ts +56 -22
  164. package/lib/media-route-response.ts +45 -0
  165. package/lib/newspaper-layout.ts +3 -2
  166. package/lib/newsroom-base-path.ts +12 -3
  167. package/lib/newsroom-board.ts +99 -0
  168. package/lib/newsroom-category-drilldown.ts +3 -2
  169. package/lib/newsroom-forum-routes.ts +6 -4
  170. package/lib/newsroom-index-filters.ts +14 -10
  171. package/lib/newsroom-nav.ts +3 -1
  172. package/lib/newsroom-organizer-nav.ts +37 -0
  173. package/lib/newsroom-section-routing.ts +20 -0
  174. package/lib/oauth-redirect-priority.ts +40 -0
  175. package/lib/papyrus-web-locations.ts +7 -6
  176. package/lib/plugin-settings.ts +11 -0
  177. package/lib/presentation-footer.ts +3 -2
  178. package/lib/pretext-layout.ts +12 -3
  179. package/lib/priming-proof-report.ts +90 -0
  180. package/lib/prospectus-publication-access.ts +53 -0
  181. package/lib/prospectus-session.ts +196 -0
  182. package/lib/prospectus-sign-in-routes.ts +284 -0
  183. package/lib/prospectus-site-identity.ts +303 -0
  184. package/lib/publication-cyclotron.ts +74 -0
  185. package/lib/rate-limit-retry.ts +55 -0
  186. package/lib/reader-base-path.ts +24 -0
  187. package/lib/reader-cache-lifetime.ts +5 -0
  188. package/lib/reader-static-params.ts +85 -4
  189. package/lib/reader-storage-url.ts +9 -12
  190. package/lib/reference-policy.ts +5 -0
  191. package/lib/relevance-decisions.ts +103 -0
  192. package/lib/relevance-status.ts +63 -0
  193. package/lib/renderer.ts +7 -0
  194. package/lib/revalidate-prewarm.ts +53 -0
  195. package/lib/revalidate-secret.ts +59 -0
  196. package/lib/robots-policy.ts +14 -0
  197. package/lib/root-route-redirect.ts +16 -0
  198. package/lib/semantic-graph.ts +7 -0
  199. package/lib/shared-credentials-provider.ts +61 -0
  200. package/lib/signup-csv.ts +24 -0
  201. package/lib/site-brand.ts +59 -0
  202. package/lib/staging-gated-path.ts +23 -0
  203. package/lib/staging-preview-object.ts +21 -5
  204. package/lib/test-editor-override.ts +4 -0
  205. package/lib/video-loading.ts +22 -0
  206. package/lib/video-script.ts +14 -0
  207. package/middleware.ts +11 -19
  208. package/package.json +73 -8
  209. package/renderers/pretext/article-page.tsx +39 -3
  210. package/renderers/pretext/presentation-shared.tsx +113 -0
  211. package/renderers/pretext/presentation-shell.tsx +15 -102
  212. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  213. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  214. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  215. package/routes.manifest.json +123 -12
  216. package/src/sync.mjs +30 -6
  217. package/src/with-papyrus.d.mts +9 -0
  218. package/src/with-papyrus.mjs +48 -3
  219. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  220. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  221. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
  222. 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
+ }
@@ -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.redirectUrls ?? DEFAULT_AUTH_REDIRECT_URLS;
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
+ }
@@ -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 = config.redirectUrls ?? DEFAULT_AUTH_REDIRECT_URLS;
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
+ }