@anthusai/papyrus 1.0.0-next.28 → 1.0.0-next.29

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.
@@ -8,6 +8,7 @@ export const PREVIEW_KEY_PREFIX = "preview/";
8
8
 
9
9
  const PRESIGN_SECONDS = 60;
10
10
  const NOT_FOUND_ERROR_NAMES = ["NotFound", "NoSuchKey"];
11
+ const FORBIDDEN_ERROR_NAMES = ["Forbidden", "AccessDenied"];
11
12
  const PREVIEW_EXCLUDED_PREFIXES = ["/newsroom", "/api", "/_next", PREVIEW_ROUTE_PREFIX];
12
13
 
13
14
  export function isStaticPreviewEnabled(environment: Record<string, string | undefined> = process.env): boolean {
@@ -32,13 +33,14 @@ export function previewCandidateKeys(requestPath: string): string[] | null {
32
33
  return [base, `${base}.html`, `${base}/index.html`];
33
34
  }
34
35
 
35
- export function shouldRewriteToPreview(pathname: string): boolean {
36
+ export function shouldRewriteToPreview(pathname: string, newsroomAtRoot: boolean = false): boolean {
36
37
  if (pathname === "/favicon.ico" || pathname === "/robots.txt" || pathname.startsWith("/icon")) return false;
37
- return !PREVIEW_EXCLUDED_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
38
+ if (PREVIEW_EXCLUDED_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))) return false;
39
+ return !newsroomAtRoot || pathname.endsWith(".html");
38
40
  }
39
41
 
40
- export function isPreviewGatedPath(pathname: string): boolean {
41
- return shouldRewriteToPreview(pathname) || pathname === PREVIEW_ROUTE_PREFIX || pathname.startsWith(`${PREVIEW_ROUTE_PREFIX}/`);
42
+ export function isPreviewGatedPath(pathname: string, newsroomAtRoot: boolean = false): boolean {
43
+ return shouldRewriteToPreview(pathname, newsroomAtRoot) || pathname === PREVIEW_ROUTE_PREFIX || pathname.startsWith(`${PREVIEW_ROUTE_PREFIX}/`);
42
44
  }
43
45
 
44
46
  export function previewRewritePathname(pathname: string): string {
@@ -54,6 +56,12 @@ function isNotFoundError(error: unknown): boolean {
54
56
  return NOT_FOUND_ERROR_NAMES.includes(name);
55
57
  }
56
58
 
59
+ function isForbiddenError(error: unknown): boolean {
60
+ const candidate = error as { name?: string; $metadata?: { httpStatusCode?: number }; metadata?: { httpStatusCode?: number } } | null;
61
+ const status = candidate?.$metadata?.httpStatusCode ?? candidate?.metadata?.httpStatusCode;
62
+ return status === 403 || FORBIDDEN_ERROR_NAMES.includes(candidate?.name ?? "");
63
+ }
64
+
57
65
  async function presignFirstExistingKey(candidateKeys: string[]): Promise<URL | null> {
58
66
  const { runWithAmplifyServerContext } = getAmplifyServerRuntime();
59
67
  return runWithAmplifyServerContext({
@@ -83,7 +91,15 @@ export async function streamPreviewObject(pathname: string): Promise<Response> {
83
91
  if (!isStaticPreviewEnabled()) return notFound();
84
92
  const candidateKeys = previewCandidateKeys(previewRequestPath(pathname));
85
93
  if (!candidateKeys) return notFound();
86
- const presigned = await presignFirstExistingKey(candidateKeys);
94
+ let presigned: URL | null;
95
+ try {
96
+ presigned = await presignFirstExistingKey(candidateKeys);
97
+ } catch (error) {
98
+ if (isForbiddenError(error)) {
99
+ return new Response("Staging preview is limited to editors and admins.", { status: 403, headers: { "Cache-Control": "private, no-store" } });
100
+ }
101
+ throw error;
102
+ }
87
103
  if (!presigned) return notFound();
88
104
  const upstream = await fetch(presigned);
89
105
  if (!upstream.ok || !upstream.body) return notFound();
package/middleware.ts CHANGED
@@ -23,7 +23,7 @@ function isStagingGatedPathForBrand(pathname: string): boolean {
23
23
  }
24
24
 
25
25
  function routeRequest(request: NextRequest): NextResponse {
26
- if (isStaticPreviewEnabled() && shouldRewriteToPreview(request.nextUrl.pathname)) {
26
+ if (isStaticPreviewEnabled() && shouldRewriteToPreview(request.nextUrl.pathname, usesNewsroomRootPaths())) {
27
27
  const rewriteUrl = request.nextUrl.clone();
28
28
  rewriteUrl.pathname = previewRewritePathname(request.nextUrl.pathname);
29
29
  return NextResponse.rewrite(rewriteUrl);
@@ -61,8 +61,8 @@ function routeRequest(request: NextRequest): NextResponse {
61
61
  export async function middleware(request: NextRequest): Promise<NextResponse> {
62
62
  let sessionResponse: NextResponse | null = null;
63
63
  const { pathname } = request.nextUrl;
64
- const gated = isStaticPreviewEnabled() && !usesNewsroomRootPaths()
65
- ? isPreviewGatedPath(pathname)
64
+ const gated = isStaticPreviewEnabled()
65
+ ? isPreviewGatedPath(pathname, usesNewsroomRootPaths())
66
66
  : isStagingGatedPathForBrand(pathname);
67
67
  if (getSiteEnv() === "staging" && gated) {
68
68
  sessionResponse = NextResponse.next();
package/package.json CHANGED
@@ -294,7 +294,7 @@
294
294
  "optional": true
295
295
  }
296
296
  },
297
- "version": "1.0.0-next.28",
297
+ "version": "1.0.0-next.29",
298
298
  "dependencies": {
299
299
  "@aws-amplify/adapter-nextjs": "^1.7.3",
300
300
  "@aws-sdk/client-cognito-identity-provider": "3.1045.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-next.28",
2
+ "version": "1.0.0-next.29",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",