Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ NEXT_PUBLIC_AWS_RUM_IDENTITY_POOL_ID=
PEPE_CACHE_TTL_MINUTES=10
PEPE_CACHE_MAX_ITEMS=500
IPFS_GATEWAY=https://ipfs.io/ipfs/

# SERVER-SIDE CLIENT IDENTIFICATION (for SSR requests)
# Used to sign server-side API requests with HMAC-SHA256
SSR_CLIENT_ID=
SSR_CLIENT_SECRET=
7 changes: 5 additions & 2 deletions .github/workflows/build-upload-deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,15 @@ jobs:
--version-label "${{ env.COMMIT_SHA }}" \
--description "${{ env.COMMIT_MESSAGE }}"

- name: Deploy new Version to ElasticBeanstalk
- name: Deploy new Version to ElasticBeanstalk with Runtime Environment Variables
run: |
aws elasticbeanstalk update-environment \
--application-name "${{ env.BEANSTALK_APP_NAME }}" \
--environment-name "${{ env.BEANSTALK_ENV_NAME }}" \
--version-label "${{ env.COMMIT_SHA }}"
--version-label "${{ env.COMMIT_SHA }}" \
--option-settings \
Namespace=aws:elasticbeanstalk:application:environment,OptionName=SSR_CLIENT_ID,Value="${{ secrets.SSR_CLIENT_ID }}" \
Namespace=aws:elasticbeanstalk:application:environment,OptionName=SSR_CLIENT_SECRET,Value="${{ secrets.SSR_CLIENT_SECRET }}"

- name: Check Elastic Beanstalk health and readiness (120s warmup then 20 retries with 60s delay)
run: |
Expand Down
1 change: 1 addition & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const fetchCache = "force-no-store";

import "@/lib/fetch/ssrFetch";
import "@/components/drops/create/lexical/lexical.styles.scss";
import "@/styles/Home.module.scss";
import "@/styles/seize-bootstrap.scss";
Expand Down
9 changes: 9 additions & 0 deletions config/serverEnv.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from "zod";

export const serverEnvSchema = z.object({
SSR_CLIENT_ID: z.string().min(1, "SSR_CLIENT_ID is required"),
SSR_CLIENT_SECRET: z.string().min(1, "SSR_CLIENT_SECRET is required"),
});

export type ServerEnv = z.infer<typeof serverEnvSchema>;

36 changes: 36 additions & 0 deletions config/serverEnv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { serverEnvSchema, type ServerEnv } from "./serverEnv.schema";

if (typeof globalThis.window !== "undefined") {
throw new TypeError("serverEnv can only be accessed on the server side");
}

const getServerEnv = (): ServerEnv | null => {
if (typeof process === "undefined" || !process.env) {
return null;
}

const raw = {
SSR_CLIENT_ID: process.env.SSR_CLIENT_ID,
SSR_CLIENT_SECRET: process.env.SSR_CLIENT_SECRET,
};

const parsed = serverEnvSchema.safeParse(raw);
if (!parsed.success) {
return null;
}

return parsed.data;
};

export const getServerEnvOrThrow = (): ServerEnv => {
const env = getServerEnv();
if (!env) {
throw new Error(
"SSR_CLIENT_ID and SSR_CLIENT_SECRET must be set as runtime environment variables"
);
}
return env;
};

export const serverEnv: ServerEnv | null = getServerEnv();

25 changes: 25 additions & 0 deletions helpers/server-signature.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createHmac } from "node:crypto";

export function generateClientSignature(
clientId: string,
secret: string,
method: string,
path: string,
timestamp?: number
): { clientId: string; timestamp: number; signature: string } {
if (typeof globalThis.window !== "undefined") {
throw new TypeError(
"generateClientSignature can only be used on the server side"
);
}

const ts = timestamp ?? Math.floor(Date.now() / 1000);
const payload = `${clientId}\n${ts}\n${method}\n${path}`;
const signature = createHmac("sha256", secret).update(payload).digest("hex");

return {
clientId,
timestamp: ts,
signature,
};
}
Comment thread
prxt6529 marked this conversation as resolved.
89 changes: 89 additions & 0 deletions lib/fetch/ssrFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { publicEnv } from "@/config/env";
import { getServerEnvOrThrow } from "@/config/serverEnv";
import { generateClientSignature } from "@/helpers/server-signature.helpers";

const originalFetch = globalThis.fetch;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const isApiRequest = (url: string | URL): boolean => {
const urlString = typeof url === "string" ? url : url.toString();
try {
const parsedUrl = new URL(urlString, publicEnv.API_ENDPOINT);
return parsedUrl.origin === new URL(publicEnv.API_ENDPOINT).origin;
} catch {
return false;
}
};

const extractPathFromUrl = (url: string | URL, baseUrl: string): string => {
const urlString = typeof url === "string" ? url : url.toString();
try {
const parsedUrl = new URL(urlString, baseUrl);
return parsedUrl.pathname + parsedUrl.search;
} catch {
return "";
}
};

const enhancedFetch: typeof fetch = async (
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> => {
if (typeof globalThis.window !== "undefined") {
return originalFetch(input, init);
}

const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;

if (!isApiRequest(url)) {
return originalFetch(input, init);
}

let clientId: string;
let clientSecret: string;

try {
const env = getServerEnvOrThrow();
clientId = env.SSR_CLIENT_ID;
clientSecret = env.SSR_CLIENT_SECRET;
} catch {
return originalFetch(input, init);
}
Comment thread
prxt6529 marked this conversation as resolved.
Outdated

const method = init?.method?.toUpperCase() ?? "GET";
const path = extractPathFromUrl(url, publicEnv.API_ENDPOINT);

if (!path) {
return originalFetch(input, init);
}

const signatureData = generateClientSignature(
clientId,
clientSecret,
method,
path
);

const enhancedHeaders = new Headers(init?.headers);
enhancedHeaders.set("x-6529-internal-id", signatureData.clientId);
enhancedHeaders.set("x-6529-internal-signature", signatureData.signature);
enhancedHeaders.set(
"x-6529-internal-timestamp",
signatureData.timestamp.toString()
);

return originalFetch(input, {
...init,
headers: enhancedHeaders,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

if (typeof globalThis.window === "undefined") {
globalThis.fetch = enhancedFetch;
}

export { enhancedFetch as ssrFetch };
Loading
Loading