Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
133 changes: 130 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions infra/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,24 @@ const hono = new sst.cloudflare.Worker("Hono", {
handler: "./packages/workers/src/api.ts",
link: [auth, authKv, cacheKv, ...allSecrets],
domain: "api." + domain,
// eventually: enable sourcemaps when this is fixed: https://github.com/sst/sst/issues/4514
// build: {
// esbuild: {
// sourcemap: true,
// plugins: [
// // Put the Sentry esbuild plugin after all other plugins
// sentryEsbuildPlugin({
// authToken: process.env.SENTRY_AUTH_TOKEN,
// org: "coder-aw",
// project: "node-cloudflare-workers",
// }),
// ],
// },
// },
transform: {
worker: {
compatibilityDate: "2024-09-23",
compatibilityFlags: ["nodejs_compat"],
// staging will bind to dev wrangler workers too
serviceBindings: [
{
Expand Down
16 changes: 13 additions & 3 deletions infra/Dns.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import type { DomainStages } from "./types";

export const STAGES = {
PROD: "prod",
STG: "stg",
UAT: "uat",
} as const satisfies DomainStages;

const BASE_DOMAIN = "semhub.dev";

export const domain =
{
prod: "semhub.dev",
stg: "stg.semhub.dev",
uat: "uat.semhub.dev",
[STAGES.PROD]: BASE_DOMAIN,
[STAGES.STG]: `stg.${BASE_DOMAIN}`,
[STAGES.UAT]: `uat.${BASE_DOMAIN}`,
}[$app.stage] || $app.stage + ".stg.semhub.dev";

// export const zone = cloudflare.getZoneOutput({
Expand Down
1 change: 1 addition & 0 deletions infra/Secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const secret = {
githubAppClientSecret: new sst.Secret("SEMHUB_GITHUB_APP_CLIENT_SECRET"),
githubAppId: new sst.Secret("SEMHUB_GITHUB_APP_ID"),
githubAppPrivateKey: new sst.Secret("SEMHUB_GITHUB_APP_PRIVATE_KEY"),
sentryAuthToken: new sst.Secret("SENTRY_AUTH_TOKEN"),
keys,
};

Expand Down
3 changes: 3 additions & 0 deletions infra/Web.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { apiUrl } from "./Api";
import { domain } from "./Dns";
import { secret } from "./Secret";

const web = new sst.aws.StaticSite("Web", {
path: "packages/web",
environment: {
// when adding new env vars, you may have to rm -rf node_modules
SENTRY_AUTH_TOKEN: secret.sentryAuthToken.value,
SST_STAGE: $app.stage,
VITE_SST_STAGE: $app.stage,
VITE_API_URL: apiUrl.apply((url) => {
if (typeof url !== "string") {
Expand Down
6 changes: 6 additions & 0 deletions infra/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ export type CronPatterns = {
readonly SYNC_ISSUE: "*/20 * * * *";
readonly SYNC_EMBEDDING: "0 * * * *";
};

export type DomainStages = {
readonly PROD: "prod";
readonly STG: "stg";
readonly UAT: "uat";
};
12 changes: 12 additions & 0 deletions packages/core/src/constants/domain.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { DomainStages } from "@/infra/types";

export const STAGES = {
PROD: "prod",
STG: "stg",
UAT: "uat",
} as const satisfies DomainStages;

export const APP_DOMAIN = "semhub.dev";
export const LOCAL_DEV_DOMAIN = `local.${APP_DOMAIN}`;
export const APP_STG_DOMAIN = `stg.${APP_DOMAIN}`;
export const APP_UAT_DOMAIN = `uat.${APP_DOMAIN}`;
4 changes: 4 additions & 0 deletions packages/core/sst-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"SENTRY_AUTH_TOKEN": {
"type": "sst.sst.Secret"
"value": string
}
"Web": {
"type": "sst.aws.StaticSite"
"url": string
Expand Down
2 changes: 2 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"@radix-ui/react-tooltip": "^1.1.4",
"@semhub/core": "workspace:*",
"@semhub/workers": "workspace:*",
"@sentry/react": "^8.54.0",
"@sentry/vite-plugin": "^3.1.2",
"@tanstack/react-form": "^0.33.0",
"@tanstack/react-query": "^5.59.6",
"@tanstack/react-router": "^1.63.5",
Expand Down
1 change: 0 additions & 1 deletion packages/web/src/components/search/HomepageSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export function HomepageSearch() {
<div className="relative">
<HomepageSearchBar />
</div>

{/* Suggested searches section */}
<div className="mx-auto mt-24 grid max-w-xl grid-cols-1 gap-2 sm:mt-8 sm:grid-cols-2 sm:gap-4">
{suggestedSearches.slice(0, 4).map((search) => (
Expand Down
43 changes: 42 additions & 1 deletion packages/web/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import "./globals.css";

import * as Counterscale from "@counterscale/tracker";
import * as Sentry from "@sentry/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { Loader2Icon } from "lucide-react";
import { ThemeProvider } from "next-themes";
import ReactDOM from "react-dom/client";

import { client } from "@/lib/api/client";
import { queryClient } from "@/lib/queryClient";

import { Error } from "./components/Error";
import { NotFound } from "./components/NotFound";
import { routeTree } from "./routeTree.gen";

const sstStage = import.meta.env.VITE_SST_STAGE;
// Initialize Counterscale analytics

// for web analytics
Counterscale.init({
siteId: `semhub-${sstStage}`,
reporterUrl: "https://semhub-prod-counterscale.pages.dev/collect",
Expand All @@ -35,6 +38,44 @@ const router = createRouter({
defaultErrorComponent: ({ error }) => <Error error={error} />,
});

Sentry.init({
dsn:
sstStage === "prod"
? "https://566d7949ee5e8265ac7d917d289585bd@o4508764596142080.ingest.us.sentry.io/4508770676637696"
: "https://bf47d2a69dccbb1f44173be530166765@o4508764596142080.ingest.us.sentry.io/4508764610494464",
environment: sstStage,
tunnel: client.sentry.tunnel.$url().toString(),
debug: sstStage === "prod" ? false : true,
integrations: [
Sentry.tanstackRouterBrowserTracingIntegration(router),
Sentry.replayIntegration({
// NOTE: This will disable built-in masking. Only use this if your site has no sensitive data, or if you've already set up other options for masking or blocking relevant data, such as 'ignore', 'block', 'mask' and 'maskFn'.
maskAllText: false,
blockAllMedia: false,
}),
],
// Tracing
tracesSampleRate: sstStage === "prod" ? 0.1 : 1.0, // Capture 10% in prod, 100% elsewhere
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: [
// uat endpoints
"https://api.uat.semhub.dev",
"https://auth.uat.semhub.dev",
// Production endpoints
"https://api.semhub.dev",
"https://auth.semhub.dev",
// stg endpoints
"https://api.stg.semhub.dev",
"https://auth.stg.semhub.dev",
// dev API endpoints with dynamic subdomains
/^https:\/\/api\.[^.]+\.stg\.semhub\.dev$/, // Matches api.{anything}.stg.semhub.dev
/^https:\/\/auth\.[^.]+\.stg\.semhub\.dev$/, // Matches auth.{anything}.stg.semhub.dev
],
// Session Replay - only enabled in production
replaysSessionSampleRate: sstStage === "prod" ? 0.1 : 0, // 10% of sessions in prod, disabled elsewhere
replaysOnErrorSampleRate: sstStage === "prod" ? 1.0 : 0, // 100% of error sessions in prod, disabled elsewhere
});

declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
Expand Down
16 changes: 14 additions & 2 deletions packages/web/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import fs from "fs";
import path from "path";
import { sentryVitePlugin } from "@sentry/vite-plugin";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig(() => {
return {
plugins: [TanStackRouterVite({}), react()],
plugins: [
TanStackRouterVite({}),
react(),
sentryVitePlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: "coder-aw",
project:
process.env.SST_STAGE === "prod"
? "semhub-web-prod"
: "semhub-web-dev",
}),
],
resolve: {
alias: {
"@/core": path.resolve(__dirname, "../core/src"),
Expand All @@ -16,6 +27,7 @@ export default defineConfig(() => {
},
},
build: {
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
Expand Down
5 changes: 3 additions & 2 deletions packages/workers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
"build": "tsc --build"
},
"dependencies": {
"@semhub/core": "workspace:*",
"@semhub/wrangler": "workspace:*",
"@hono/zod-validator": "^0.4.1",
"@openauthjs/openauth": "*",
"@semhub/core": "workspace:*",
"@semhub/wrangler": "workspace:*",
"@sentry/cloudflare": "^8.54.0",
"hono": "^4.6.5"
},
"devDependencies": {
Expand Down
27 changes: 25 additions & 2 deletions packages/workers/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import type { ExportedHandler } from "@cloudflare/workers-types";
import * as Sentry from "@sentry/cloudflare";
import { Resource } from "sst";

import type { Context } from "./server/app";
import { app } from "./server/app";

export default {
fetch: app.fetch,
type CloudflareRequest<T = unknown> = Request<T, CfProperties<T>>;
const handler: ExportedHandler<Context> = {
async fetch(request, env, ctx) {
// needed to fix type error
return app.fetch(request as CloudflareRequest, env, ctx);
},
};

// Wrap with Sentry
export default Sentry.withSentry(
() => ({
dsn:
Resource.App.stage === "prod"
? "https://8a5572abfbb6f99f6144edf73b98446f@o4508764596142080.ingest.us.sentry.io/4508770682273792"
: "https://d415d30f99a3f43649f2289a054fe5b2@o4508764596142080.ingest.us.sentry.io/4508764598829056",
tracesSampleRate: Resource.App.stage === "prod" ? 0.2 : 1.0,
debug: Resource.App.stage === "prod" ? false : true,
environment: Resource.App.stage,
}),
handler,
);
41 changes: 28 additions & 13 deletions packages/workers/src/auth/auth.constant.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { type cors } from "hono/cors";
import type { CookieOptions } from "hono/utils/cookie";

import {
APP_DOMAIN,
APP_STG_DOMAIN,
APP_UAT_DOMAIN,
LOCAL_DEV_DOMAIN,
STAGES,
} from "@/core/constants/domain.constant";
import { GITHUB_SCOPES_PERMISSION } from "@/core/github/permission/oauth";

// Extract CORSOptions type from cors function
type CORSOptions = NonNullable<Parameters<typeof cors>[0]>;

export const githubLogin = {
provider: "github-login" as const,
scopes: [
Expand All @@ -10,18 +21,13 @@ export const githubLogin = {
],
};

export const APP_DOMAIN = "semhub.dev";
const LOCAL_DEV_DOMAIN = `local.${APP_DOMAIN}`;
const APP_STG_DOMAIN = `stg.${APP_DOMAIN}`;
const APP_UAT_DOMAIN = `uat.${APP_DOMAIN}`;

function getCookieDomain(stage: string) {
switch (stage) {
case "prod":
case STAGES.PROD:
return APP_DOMAIN;
case "uat":
case STAGES.UAT:
return APP_UAT_DOMAIN;
case "stg":
case STAGES.STG:
return APP_STG_DOMAIN;
default:
// For local development, we set the cookie on the parent domain (.semhub.dev) because:
Expand All @@ -34,7 +40,7 @@ function getCookieDomain(stage: string) {
}

function isLocalDev(stage: string): boolean {
return stage !== "prod" && stage !== "stg" && stage !== "uat";
return stage !== STAGES.PROD && stage !== STAGES.STG && stage !== STAGES.UAT;
}

export function getCookieOptions(stage: string): CookieOptions {
Expand All @@ -55,11 +61,15 @@ export function getAuthServerCORS() {
credentials: false,
// can use wildcard if credentials: false is used
origin: `https://*.${APP_DOMAIN}`,
allowHeaders: ["Content-Type"],
allowHeaders: [
"Content-Type",
"sentry-trace", // Allow Sentry tracing headers
"baggage", // Allow Sentry baggage header
],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length", "Access-Control-Allow-Origin"],
maxAge: 600,
};
} satisfies CORSOptions;
}

export function getApiServerCORS(stage: string) {
Expand All @@ -76,9 +86,14 @@ export function getApiServerCORS(stage: string) {
return {
credentials: true,
origin: origins,
allowHeaders: ["Content-Type", "Authorization"],
allowHeaders: [
"Content-Type",
"Authorization",
"sentry-trace", // Allow Sentry tracing headers
"baggage", // Allow Sentry baggage header
],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length", "Access-Control-Allow-Origin"],
maxAge: 600,
};
} satisfies CORSOptions;
}
3 changes: 2 additions & 1 deletion packages/workers/src/auth/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { Hono } from "hono";
import { cors } from "hono/cors";
import { Resource } from "sst";

import { APP_DOMAIN } from "@/core/constants/domain.constant";
import { tokensetRawSchema } from "@/core/github/schema.oauth";
import { User } from "@/core/user";
import { parseHostname } from "@/core/util/url";

import { getDeps } from "../deps";
import { APP_DOMAIN, getAuthServerCORS, githubLogin } from "./auth.constant";
import { getAuthServerCORS, githubLogin } from "./auth.constant";
import { subjects } from "./subjects";

const app = new Hono();
Expand Down
Loading