Remove dotenv and inject runtime env

This commit is contained in:
2026-06-17 01:58:24 -07:00
parent 3cac9576fe
commit 7435ff6da6
145 changed files with 636 additions and 402 deletions

View File

@@ -514,7 +514,6 @@
"date-fns": "^4.1.0",
"decimal.js": "^10.5.0",
"detect-content-type": "^1.2.0",
"dotenv": "^16.5.0",
"drizzle-orm": "catalog:",
"express": "^4.21.1",
"express-rate-limit": "^7.5.1",

View File

@@ -1,4 +1,3 @@
import "dotenv/config";
import { loadLocalEnv } from "../src/utils/envUtils";
loadLocalEnv();

View File

@@ -115,7 +115,6 @@
"date-fns": "^4.1.0",
"decimal.js": "^10.5.0",
"detect-content-type": "^1.2.0",
"dotenv": "^16.5.0",
"drizzle-orm": "catalog:",
"express": "^4.21.1",
"express-rate-limit": "^7.5.1",

View File

@@ -1,4 +1,3 @@
import "dotenv/config";
import { loadLocalEnv } from "./src/utils/envUtils";
import Stripe from "stripe";
import {

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import "../sentry.ts";
import { CronJob } from "cron";
import { initDrizzle } from "../db/initDrizzle.js";
@@ -37,7 +38,7 @@ const logCronHeartbeat = () => {
};
const main = async () => {
if (process.env.DISABLE_CRON === "true") {
if (runtimeEnv.DISABLE_CRON === "true") {
console.log(`Cron disabled!`);
return;
}

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { getTableColumns, type SQL, sql } from "drizzle-orm";
import type { PgTable } from "drizzle-orm/pg-core";
@@ -115,7 +116,7 @@ export const isConnectionDropError = ({
};
/** Throws if the connection string looks like a production database. Single source of truth for this check. */
export const assertNotProductionDb = (url = process.env.DATABASE_URL || "") => {
export const assertNotProductionDb = (url = runtimeEnv.DATABASE_URL || "") => {
if (url.includes("us-east-2")) {
throw new Error(
"Refusing to run against production database (connection string contains us-east-2)",

View File

@@ -1,7 +1,4 @@
import dotenv from "dotenv";
dotenv.config();
import { runtimeEnv } from "@/utils/envUtils.js";
import { schemas as schema } from "@autumn/shared";
import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
@@ -58,8 +55,8 @@ export const initDrizzle = ({
name?: string;
} = {}) => {
const envDbUrl = replica
? process.env.DATABASE_REPLICA_URL
: process.env.DATABASE_URL;
? runtimeEnv.DATABASE_REPLICA_URL
: runtimeEnv.DATABASE_URL;
const dbUrl = databaseUrl || envDbUrl || "";
@@ -96,7 +93,7 @@ export const initDrizzle = ({
};
// Strict latency limits in prod; relaxed locally so dev pool warm-up doesn't kill tests.
const isProd = process.env.NODE_ENV === "production";
const isProd = runtimeEnv.NODE_ENV === "production";
const poolMaxFromEnv = ({
envVar,
@@ -105,7 +102,7 @@ const poolMaxFromEnv = ({
envVar: string;
fallback: number;
}): number => {
const parsed = Number(process.env[envVar]);
const parsed = Number(runtimeEnv[envVar]);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
};
@@ -151,7 +148,7 @@ export const { db: dbCritical, client: clientCritical } = initDrizzle({
name: "critical",
maxConnections: criticalPoolMax,
connectTimeout: isProd ? 2 : 30,
databaseUrl: process.env.DATABASE_CRITICAL_URL,
databaseUrl: runtimeEnv.DATABASE_CRITICAL_URL,
poolConfig: {
application_name: "autumn-critical",
query_timeout: isProd ? 2_000 : 30_000,
@@ -169,7 +166,7 @@ export const { db: dbGeneral, client: clientGeneral } = initDrizzle({
// -- Replica pool: used as fallback when primary is degraded --
// Only created if DATABASE_REPLICA_URL is configured.
const replicaResult = process.env.DATABASE_REPLICA_URL
const replicaResult = runtimeEnv.DATABASE_REPLICA_URL
? initDrizzle({
name: "replica",
replica: true,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { SQL } from "drizzle-orm";
import type { Pool } from "pg";
import { logger } from "@/external/logtail/logtailUtils.js";
@@ -243,7 +244,7 @@ export const getPgHealthState = (): {
failureCount,
probeActive: probeInterval !== null,
firstProbeSuccessAt,
hasReplica: !!process.env.DATABASE_REPLICA_URL,
hasReplica: !!runtimeEnv.DATABASE_REPLICA_URL,
});
/** Force DEGRADED state (for testing). Does NOT start the recovery probe. */

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { Pool } from "pg";
import { logger } from "@/external/logtail/logtailUtils.js";
@@ -11,8 +12,8 @@ const registry = new Map<string, RegisteredPool>();
let snapshotInterval: ReturnType<typeof setInterval> | null = null;
const getRole = (): string => {
if (process.env.WORKER === "true") return "worker";
if (process.env.CRON === "true") return "cron";
if (runtimeEnv.WORKER === "true") return "worker";
if (runtimeEnv.CRON === "true") return "cron";
return "http";
};

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { createHash } from "node:crypto";
const hash = (value: string) =>
@@ -33,9 +34,9 @@ export const redactDatabaseUrl = (databaseUrl?: string) => {
};
export const getRedactedDatabaseUrls = () => ({
primary: redactDatabaseUrl(process.env.DATABASE_URL),
replica: redactDatabaseUrl(process.env.DATABASE_REPLICA_URL),
primary: redactDatabaseUrl(runtimeEnv.DATABASE_URL),
replica: redactDatabaseUrl(runtimeEnv.DATABASE_REPLICA_URL),
critical: redactDatabaseUrl(
process.env.DATABASE_CRITICAL_URL || process.env.DATABASE_URL,
runtimeEnv.DATABASE_CRITICAL_URL || runtimeEnv.DATABASE_URL,
),
});

View File

@@ -1,7 +1,8 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { createAnthropic } from "@ai-sdk/anthropic";
export const anthropicClient = process.env.ANTHROPIC_API_KEY
export const anthropicClient = runtimeEnv.ANTHROPIC_API_KEY
? createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
apiKey: runtimeEnv.ANTHROPIC_API_KEY,
})
: undefined;

View File

@@ -1,8 +1,5 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: AutumnInt is used for internal testing & scripts */
import dotenv from "dotenv";
dotenv.config();
import { runtimeEnv } from "@/utils/envUtils.js";
import {
type ApiBaseEntity,
type ApiCusFeatureV3,
@@ -93,9 +90,9 @@ export class AutumnInt {
liveUrl?: boolean;
skipCacheDeletion?: boolean;
} = {}) {
// this.apiKey = apiKey || process.env.AUTUMN_API_KEY || "";
// this.apiKey = apiKey || runtimeEnv.AUTUMN_API_KEY || "";
this.apiKey =
apiKey || secretKey || process.env.UNIT_TEST_AUTUMN_SECRET_KEY || "";
apiKey || secretKey || runtimeEnv.UNIT_TEST_AUTUMN_SECRET_KEY || "";
this.headers = {
Authorization: `Bearer ${this.apiKey}`,
@@ -110,7 +107,7 @@ export class AutumnInt {
this.headers["org-config"] = JSON.stringify(orgConfig);
}
const envBase = process.env.AUTUMN_TEST_BASE_URL;
const envBase = runtimeEnv.AUTUMN_TEST_BASE_URL;
const envBaseUrl = envBase ? `${envBase.replace(/\/$/, "")}/v1` : null;
this.baseUrl =
baseUrl ||
@@ -316,7 +313,7 @@ export class AutumnInt {
Object.keys(headers).length > 0 ? headers : undefined,
);
const concurrency = Number(process.env.TEST_FILE_CONCURRENCY || "0");
const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
const finalTimeout = timeout ?? defaultTimeout;
if (finalTimeout) {
@@ -1219,7 +1216,7 @@ export class AutumnInt {
Object.keys(headers).length > 0 ? headers : undefined,
);
const concurrency = Number(process.env.TEST_FILE_CONCURRENCY || "0");
const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
const finalTimeout = timeout ?? defaultTimeout;
if (finalTimeout) {
@@ -1257,7 +1254,7 @@ export class AutumnInt {
): Promise<TResponse> => {
const data = await this.post(`/billing.create_schedule`, params);
const concurrency = Number(process.env.TEST_FILE_CONCURRENCY || "0");
const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
const finalTimeout = timeout ?? defaultTimeout;
if (finalTimeout) {
@@ -1288,7 +1285,7 @@ export class AutumnInt {
Object.keys(headers).length > 0 ? headers : undefined,
);
const concurrency = Number(process.env.TEST_FILE_CONCURRENCY || "0");
const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
const finalTimeout = timeout ?? defaultTimeout;
if (finalTimeout) {

View File

@@ -1,8 +1,5 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: RPC test client needs flexible payload typing */
import dotenv from "dotenv";
dotenv.config();
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, type OrgConfig } from "@autumn/shared";
import AutumnError from "./autumnCli.js";
@@ -27,7 +24,7 @@ export class AutumnRpcCli {
liveUrl?: boolean;
} = {}) {
this.apiKey =
apiKey || secretKey || process.env.UNIT_TEST_AUTUMN_SECRET_KEY || "";
apiKey || secretKey || runtimeEnv.UNIT_TEST_AUTUMN_SECRET_KEY || "";
this.headers = {
Authorization: `Bearer ${this.apiKey}`,
@@ -42,7 +39,7 @@ export class AutumnRpcCli {
this.headers["org-config"] = JSON.stringify(orgConfig);
}
const envBase = process.env.AUTUMN_TEST_BASE_URL;
const envBase = runtimeEnv.AUTUMN_TEST_BASE_URL;
const envBaseUrl = envBase ? `${envBase.replace(/\/$/, "")}/v1` : null;
this.baseUrl =
baseUrl ||

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode } from "@autumn/shared";
import { Hono } from "hono";
import { Webhook } from "svix";
@@ -17,7 +18,7 @@ const verifyAutumnWebhook = async ({
svixSignature: string | undefined;
};
}) => {
const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!);
const wh = new Webhook(runtimeEnv.AUTUMN_WEBHOOK_SECRET!);
const { svixId, svixTimestamp, svixSignature } = headers;

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { z } from "zod/v4";
/**
@@ -55,9 +56,9 @@ export const resolveAwsTaskIdentity = async (): Promise<AwsTaskIdentity> => {
identityPromise = (async (): Promise<AwsTaskIdentity> => {
const imageSha =
process.env.FC_GIT_COMMIT_SHA || process.env.IMAGE_TAG || null;
runtimeEnv.FC_GIT_COMMIT_SHA || runtimeEnv.IMAGE_TAG || null;
const metadataUri = process.env.ECS_CONTAINER_METADATA_URI_V4;
const metadataUri = runtimeEnv.ECS_CONTAINER_METADATA_URI_V4;
let serviceArn: string | null = null;
if (metadataUri) {
@@ -94,7 +95,7 @@ export const resolveAwsTaskIdentity = async (): Promise<AwsTaskIdentity> => {
`[awsTaskIdentity] ECS metadata fetch failed: ${error instanceof Error ? error.message : error}; gate will fail open`,
);
}
} else if (process.env.NODE_ENV === "production") {
} else if (runtimeEnv.NODE_ENV === "production") {
console.warn(
"[awsTaskIdentity] ECS_CONTAINER_METADATA_URI_V4 unset in production — gate will fail open",
);

View File

@@ -5,5 +5,6 @@
* so it's the canonical "am I on AWS?" gate — same one
* `awsTaskIdentity` uses to discover the running service.
*/
import { runtimeEnv } from "@/utils/envUtils.js";
export const onAwsEcs = (): boolean =>
Boolean(process.env.ECS_CONTAINER_METADATA_URI_V4);
Boolean(runtimeEnv.ECS_CONTAINER_METADATA_URI_V4);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
CreateScheduleCommand,
DeleteScheduleCommand,
@@ -8,14 +9,14 @@ import { extractLocalEndpoint } from "@/queue/initSqs.js";
import { schedulerClient } from "./initEventBridge.js";
const isLocalQueue = (): boolean =>
!!extractLocalEndpoint({ queueUrl: process.env.SQS_QUEUE_URL_V2 });
!!extractLocalEndpoint({ queueUrl: runtimeEnv.SQS_QUEUE_URL_V2 });
const SCHEDULE_GROUP = "default";
const SCHEDULER_ROLE_ARN = process.env.AWS_EVENTBRIDGE_SCHEDULER_ROLE_ARN || "";
const SCHEDULER_ROLE_ARN = runtimeEnv.AWS_EVENTBRIDGE_SCHEDULER_ROLE_ARN || "";
/** Derives SQS ARN from URL: https://sqs.<region>.amazonaws.com/<account>/<name> -> arn:aws:sqs:<region>:<account>:<name> */
const getSqsQueueArn = (): string => {
const url = process.env.SQS_QUEUE_URL_V2 || "";
const url = runtimeEnv.SQS_QUEUE_URL_V2 || "";
const match = url.match(
/^https:\/\/sqs\.([a-z0-9-]+)\.amazonaws\.com\/(\d+)\/(.+)$/,
);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { SchedulerClient } from "@aws-sdk/client-scheduler";
import {
DEFAULT_AWS_REGION,
@@ -7,11 +8,11 @@ import {
const getSchedulerClientConfig = () => ({
region:
extractRegionFromQueueUrl({
queueUrl: process.env.SQS_QUEUE_URL_V2,
queueUrl: runtimeEnv.SQS_QUEUE_URL_V2,
}) || DEFAULT_AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
accessKeyId: runtimeEnv.AWS_ACCESS_KEY_ID || "",
secretAccessKey: runtimeEnv.AWS_SECRET_ACCESS_KEY || "",
},
});

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
export const ADMIN_REQUEST_BLOCK_CONFIG_KEY = "admin/request-block-config.json";
export const ADMIN_ROLLOUT_CONFIG_KEY = "admin/rollout-config.json";
export const ADMIN_FEATURE_FLAGS_CONFIG_KEY = "admin/feature-flags-config.json";
@@ -21,8 +22,8 @@ export const BLUE_GREEN_CRON_ACTIVE_SLOT_KEY =
"admin/blue-green-cron-active-slot.json";
export const BLUE_GREEN_HEARTBEAT_KEY_PREFIX = "admin/blue-green-heartbeats";
const bucket = process.env.S3_BUCKET || "autumn-prod-server";
const region = process.env.S3_REGION || "us-east-2";
const bucket = runtimeEnv.S3_BUCKET || "autumn-prod-server";
const region = runtimeEnv.S3_REGION || "us-east-2";
export const getAdminS3Config = () => {
return {

View File

@@ -1,7 +1,8 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { Axiom } from "@axiomhq/js";
const AXIOM_ADMIN_TOKEN = process.env.AXIOM_ADMIN_TOKEN;
const AXIOM_ORG_ID = process.env.AXIOM_ORG_ID;
const AXIOM_ADMIN_TOKEN = runtimeEnv.AXIOM_ADMIN_TOKEN;
const AXIOM_ORG_ID = runtimeEnv.AXIOM_ORG_ID;
export const axiomClient: Axiom | null = AXIOM_ADMIN_TOKEN
? new Axiom({

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv, InternalError, type Organization } from "@autumn/shared";
import type { DrizzleCli } from "@server/db/initDrizzle.js";
import { OrgService } from "@server/internal/orgs/OrgService.js";
@@ -42,8 +43,8 @@ export const deauthorizeAccount = async ({
await masterStripe.oauth.deauthorize({
client_id:
env === AppEnv.Live
? process.env.STRIPE_LIVE_CLIENT_ID || ""
: process.env.STRIPE_SANDBOX_CLIENT_ID || "",
? runtimeEnv.STRIPE_LIVE_CLIENT_ID || ""
: runtimeEnv.STRIPE_SANDBOX_CLIENT_ID || "",
stripe_user_id: accountId,
});
logger.info(`Deauthorized account ${accountId} for ${env}`);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
AppEnv,
InternalError,
@@ -6,7 +7,6 @@ import {
} from "@autumn/shared";
import { decryptData } from "@server/utils/encryptUtils.js";
import { instrumentStripe } from "@server/utils/otel/instrumentStripe.js";
import "dotenv/config";
import type { DrizzleCli } from "@server/db/initDrizzle.js";
import Stripe from "stripe";
import {
@@ -25,19 +25,19 @@ export const initMasterStripe = (params?: {
let secretKey: string;
if (params?.env === AppEnv.Live) {
if (!process.env.STRIPE_LIVE_SECRET_KEY) {
if (!runtimeEnv.STRIPE_LIVE_SECRET_KEY) {
throw new InternalError({
message: "STRIPE_LIVE_SECRET_KEY env variable is not set",
});
}
secretKey = process.env.STRIPE_LIVE_SECRET_KEY;
secretKey = runtimeEnv.STRIPE_LIVE_SECRET_KEY;
} else {
if (!process.env.STRIPE_SANDBOX_SECRET_KEY) {
if (!runtimeEnv.STRIPE_SANDBOX_SECRET_KEY) {
throw new InternalError({
message: "STRIPE_SANDBOX_SECRET_KEY env variable is not set",
});
}
secretKey = process.env.STRIPE_SANDBOX_SECRET_KEY;
secretKey = runtimeEnv.STRIPE_SANDBOX_SECRET_KEY;
}
const cacheKey = buildMasterCacheKey({
@@ -56,7 +56,9 @@ export const initMasterStripe = (params?: {
? ("2025-02-24.acacia" as any)
: undefined,
});
return params?.skipInstrumentation ? client : instrumentStripe({ client });
return params?.skipInstrumentation
? client
: instrumentStripe({ client });
},
});
};
@@ -136,9 +138,9 @@ export const getStripeWebhookSecret = async ({
let secret: string;
if (env === AppEnv.Live) {
secret = process.env.STRIPE_LIVE_WEBHOOK_SECRET || "";
secret = runtimeEnv.STRIPE_LIVE_WEBHOOK_SECRET || "";
} else {
secret = process.env.STRIPE_SANDBOX_WEBHOOK_SECRET || "";
secret = runtimeEnv.STRIPE_SANDBOX_WEBHOOK_SECRET || "";
}
if (!secret) {

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type Stripe from "stripe";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
@@ -15,7 +16,7 @@ export const registerConnectWebhook = async ({
const stripeCli = initPlatformStripe({ masterOrg: org, env });
const curWebhookEndpoints = await stripeCli.webhookEndpoints.list();
const backendUrl = process.env.SERVER_URL || process.env.STRIPE_WEBHOOK_URL;
const backendUrl = runtimeEnv.SERVER_URL || runtimeEnv.STRIPE_WEBHOOK_URL;
const webhookUrl = `${backendUrl}/webhooks/connect/${env}?org_id=${org.id}`;

View File

@@ -1,5 +1,6 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1";
export const isHatchetEnabled = !!process.env.HATCHET_CLIENT_TOKEN;
export const isHatchetEnabled = !!runtimeEnv.HATCHET_CLIENT_TOKEN;
export const hatchet = isHatchetEnabled ? HatchetClient.init() : null;

View File

@@ -3,9 +3,10 @@
* time via `syncEnvVars` to push secrets to the cloud env. Kept SDK-free
* so trigger.config.ts can import it without bloating the build.
*
* Runtime code uses `initInfisical` (SDK-based, populates process.env).
* Runtime code uses `initInfisical` (SDK-based, populates runtimeEnv).
*/
import { runtimeEnv } from "@/utils/envUtils.js";
export type InfisicalSyncEnvVar = { name: string; value: string };
export type FetchInfisicalSecretsArgs = {
@@ -106,10 +107,10 @@ export const fetchInfisicalSecretsFromEnv = (
ctxEnv: Record<string, string | undefined> = {},
): Promise<InfisicalSyncEnvVar[]> =>
fetchInfisicalSecrets({
clientId: process.env.INFISICAL_CLIENT_ID ?? ctxEnv.INFISICAL_CLIENT_ID,
clientId: runtimeEnv.INFISICAL_CLIENT_ID ?? ctxEnv.INFISICAL_CLIENT_ID,
clientSecret:
process.env.INFISICAL_CLIENT_SECRET ?? ctxEnv.INFISICAL_CLIENT_SECRET,
projectId: process.env.INFISICAL_PROJECT_ID ?? ctxEnv.INFISICAL_PROJECT_ID,
runtimeEnv.INFISICAL_CLIENT_SECRET ?? ctxEnv.INFISICAL_CLIENT_SECRET,
projectId: runtimeEnv.INFISICAL_PROJECT_ID ?? ctxEnv.INFISICAL_PROJECT_ID,
environment:
process.env.INFISICAL_ENVIRONMENT ?? ctxEnv.INFISICAL_ENVIRONMENT,
runtimeEnv.INFISICAL_ENVIRONMENT ?? ctxEnv.INFISICAL_ENVIRONMENT,
});

View File

@@ -1,18 +1,19 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { InfisicalSDK } from "@infisical/sdk";
import { loadLocalEnv } from "@/utils/envUtils.js";
import { mask } from "@/utils/genUtils";
/**
* Initialize Infisical and load secrets into process.env
* This allows all existing code using process.env to work seamlessly
* Initialize Infisical and load secrets into runtimeEnv
* This allows all existing code using runtimeEnv to work seamlessly
*/
export const initInfisical = async (params?: { secretPath?: string }) => {
loadLocalEnv();
// Only initialize if credentials are provided
const clientId = process.env.INFISICAL_CLIENT_ID;
const clientSecret = process.env.INFISICAL_CLIENT_SECRET;
const projectId = process.env.INFISICAL_PROJECT_ID;
const environment = process.env.INFISICAL_ENVIRONMENT;
const clientId = runtimeEnv.INFISICAL_CLIENT_ID;
const clientSecret = runtimeEnv.INFISICAL_CLIENT_SECRET;
const projectId = runtimeEnv.INFISICAL_PROJECT_ID;
const environment = runtimeEnv.INFISICAL_ENVIRONMENT;
if (!clientId || !clientSecret || !projectId || !environment) {
console.log("⚠️ Infisical credentials not found - skipping initialization");
@@ -37,8 +38,8 @@ export const initInfisical = async (params?: { secretPath?: string }) => {
recursive: true,
});
// Load secrets into process.env
// Note: Existing process.env variables take precedence (won't be overridden)
// Load secrets into runtimeEnv
// Note: Existing runtimeEnv variables take precedence (won't be overridden)
let loadedCount = 0;
for (const secret of allSecrets.secrets) {
@@ -48,22 +49,22 @@ export const initInfisical = async (params?: { secretPath?: string }) => {
`Retrieving restricted secret: ${secret.secretKey}, Path: ${secret.secretPath}, value: ${mask(secret.secretValue, 3, 2)}`,
);
}
if (!process.env[secret.secretKey]) {
process.env[secret.secretKey] = secret.secretValue;
if (!runtimeEnv[secret.secretKey]) {
runtimeEnv[secret.secretKey] = secret.secretValue;
loadedCount++;
}
}
for (const importSecrets of allSecrets?.imports ?? []) {
for (const importSecret of importSecrets.secrets) {
if (!process.env[importSecret.secretKey]) {
process.env[importSecret.secretKey] = importSecret.secretValue;
if (!runtimeEnv[importSecret.secretKey]) {
runtimeEnv[importSecret.secretKey] = importSecret.secretValue;
loadedCount++;
}
}
}
console.log(`✅ Infisical: loaded ${loadedCount} secrets into process.env`);
console.log(`✅ Infisical: loaded ${loadedCount} secrets into runtimeEnv`);
} catch (error) {
console.error("❌ Failed to initialize Infisical:", error);
throw error;

View File

@@ -1,9 +1,8 @@
import "dotenv/config";
import type pino from "pino";
import { initLogger } from "@/utils/logging/initLogger";
import { runtimeEnv } from "@/utils/envUtils.js";
const pinoLogger = initLogger();
const pinoLogger = initLogger({}, runtimeEnv);
const createLogMethod = (pinoMethod: any, logtailMethod?: any) => {
function rewriteAppPath(str: string) {
@@ -73,7 +72,10 @@ const createLogMethod = (pinoMethod: any, logtailMethod?: any) => {
};
};
const createLoggerStructure = (basePinoLogger: pino.Logger): Logger => ({
const createLoggerStructure = (
basePinoLogger: pino.Logger,
env: Env,
): Logger => ({
debug: createLogMethod(basePinoLogger.debug.bind(basePinoLogger)),
info: createLogMethod(basePinoLogger.info.bind(basePinoLogger)),
warn: createLogMethod(basePinoLogger.warn.bind(basePinoLogger)),
@@ -85,16 +87,20 @@ const createLoggerStructure = (basePinoLogger: pino.Logger): Logger => ({
context: any;
onlyProd?: boolean;
}) => {
if (onlyProd && process.env.NODE_ENV !== "production") {
return createLoggerStructure(basePinoLogger);
if (onlyProd && (env.NODE_ENV as string | undefined) !== "production") {
return createLoggerStructure(basePinoLogger, env);
}
const childPinoLogger = basePinoLogger.child(context);
return createLoggerStructure(childPinoLogger);
return createLoggerStructure(childPinoLogger, env);
},
});
export const createLogger = () => createLoggerStructure(pinoLogger);
export const createLogger = (env: Env) =>
createLoggerStructure(
env === runtimeEnv ? pinoLogger : initLogger({}, env),
env,
);
/**
* Lazy dual-output logger (stdout JSON + axiom). Used only by long-running
@@ -102,12 +108,16 @@ export const createLogger = () => createLoggerStructure(pinoLogger);
* our axiom store. Default `logger` / `createLogger` are unaffected.
*/
let dualPinoLogger: pino.Logger | null = null;
export const createDualLogger = () => {
if (!dualPinoLogger) dualPinoLogger = initLogger({ mode: "dual" });
return createLoggerStructure(dualPinoLogger);
export const createDualLogger = (env: Env) => {
if (env !== runtimeEnv) {
return createLoggerStructure(initLogger({ mode: "dual" }, env), env);
}
if (!dualPinoLogger)
dualPinoLogger = initLogger({ mode: "dual" }, runtimeEnv);
return createLoggerStructure(dualPinoLogger, runtimeEnv);
};
export const logger = createLogger();
export const logger = createLogger(runtimeEnv);
export type Logger = {
debug: (...args: any[]) => void;
info: (...args: any[]) => void;

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { onAwsEcs } from "@/external/aws/ecs/onAwsEcs.js";
/**
@@ -20,10 +21,10 @@ import { onAwsEcs } from "@/external/aws/ecs/onAwsEcs.js";
export const getReachableDragonflyUrl = (url: string): string => {
if (onAwsEcs()) return url;
const privateUrl = process.env.CACHE_V2_DRAGONFLY_URL?.trim();
const privateUrl = runtimeEnv.CACHE_V2_DRAGONFLY_URL?.trim();
if (!privateUrl || url.trim() !== privateUrl) return url;
const publicUrl = process.env.CACHE_V2_DRAGONFLY_PUBLIC_URL?.trim();
const publicUrl = runtimeEnv.CACHE_V2_DRAGONFLY_PUBLIC_URL?.trim();
if (!publicUrl) return url;
return publicUrl;

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { Redis } from "ioredis";
import { logger } from "@/external/logtail/logtailUtils.js";
import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js";
@@ -12,7 +13,7 @@ import {
supportsUpstashShebangForRedisV2,
} from "./initUtils/redisV2Config.js";
const rawDragonflyUrl = process.env.CACHE_V2_DRAGONFLY_URL?.trim();
const rawDragonflyUrl = runtimeEnv.CACHE_V2_DRAGONFLY_URL?.trim();
const dragonflyUrl = rawDragonflyUrl
? getReachableDragonflyUrl(rawDragonflyUrl)
: undefined;
@@ -27,8 +28,8 @@ export const redisV2: Redis = createRedisConnection({
});
const alternateInstanceUrls: Partial<Record<RedisV2InstanceName, string>> = {
upstash: process.env.CACHE_V2_UPSTASH_URL?.trim() || undefined,
redis: process.env.CACHE_V2_REDIS_URL?.trim() || undefined,
upstash: runtimeEnv.CACHE_V2_UPSTASH_URL?.trim() || undefined,
redis: runtimeEnv.CACHE_V2_REDIS_URL?.trim() || undefined,
dragonfly: dragonflyUrl,
};

View File

@@ -1,10 +1,11 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { Redis } from "ioredis";
import { instrumentRedis } from "../otel/instrumentRedis.js";
import { cacheBackupUrl } from "./redisConfig.js";
import { registerRedisCommands } from "./registerRedisCommands.js";
const REDIS_COMMAND_TIMEOUT_MS =
process.env.NODE_ENV === "production" ? 10_000 : 60_000;
runtimeEnv.NODE_ENV === "production" ? 10_000 : 60_000;
const formatRedisEndpoint = ({ cacheUrl }: { cacheUrl: string }) => {
try {
@@ -36,8 +37,8 @@ export const createRedisClient = ({
const instance = new Redis(cacheUrl, {
tls:
process.env.CACHE_CERT && !cacheBackupUrl
? { ca: process.env.CACHE_CERT }
runtimeEnv.CACHE_CERT && !cacheBackupUrl
? { ca: runtimeEnv.CACHE_CERT }
: undefined,
family: 4,
keepAlive: 10000,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { Redis } from "ioredis";
import { createDisabledRedis, createRedisClient } from "./createRedisClient.js";
import {
@@ -8,12 +9,14 @@ import {
primaryCacheUrl,
} from "./redisConfig.js";
if (process.env.CACHE_BACKUP_URL?.trim()) {
if (runtimeEnv.CACHE_BACKUP_URL?.trim()) {
console.log(
`[Redis] Using CACHE_BACKUP_URL for all regions (primary region: ${currentRegion})`,
);
} else if (!hasRedisConfig) {
console.warn("[Redis] No Redis URL configured. Running in Postgres-only mode.");
console.warn(
"[Redis] No Redis URL configured. Running in Postgres-only mode.",
);
} else if (primaryCacheUrl && getCacheUrlForRegion({ region: currentRegion })) {
console.log(`Using regional cache: ${currentRegion}`);
}

View File

@@ -1,4 +1,5 @@
// Region constants
import { runtimeEnv } from "@/utils/envUtils.js";
const REGION_US_EAST_2 = "us-east-2";
const REGION_US_WEST_2 = "us-west-2";
@@ -6,9 +7,9 @@ const REGION_US_WEST_2 = "us-west-2";
const ALL_REGIONS = [REGION_US_EAST_2, REGION_US_WEST_2] as const;
// Current region this instance is running in
export const currentRegion = process.env.AWS_REGION || REGION_US_WEST_2;
export const currentRegion = runtimeEnv.AWS_REGION || REGION_US_WEST_2;
export const cacheBackupUrl = process.env.CACHE_BACKUP_URL?.trim();
export const cacheBackupUrl = runtimeEnv.CACHE_BACKUP_URL?.trim();
// Map of region to cache URL. When CACHE_BACKUP_URL is set, all regions use it
// (failover / single backup endpoint).
@@ -18,12 +19,12 @@ const regionToCacheUrl: Record<string, string | undefined> = cacheBackupUrl
[REGION_US_WEST_2]: cacheBackupUrl,
}
: {
[REGION_US_EAST_2]: process.env.CACHE_URL_US_EAST,
[REGION_US_WEST_2]: process.env.CACHE_URL,
[REGION_US_EAST_2]: runtimeEnv.CACHE_URL_US_EAST,
[REGION_US_WEST_2]: runtimeEnv.CACHE_URL,
};
export const primaryCacheUrl =
regionToCacheUrl[currentRegion] || process.env.CACHE_URL || cacheBackupUrl;
regionToCacheUrl[currentRegion] || runtimeEnv.CACHE_URL || cacheBackupUrl;
export const hasRedisConfig = Boolean(primaryCacheUrl);
/** Get all regions that have configured cache URLs */

View File

@@ -1,7 +1,8 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js";
export const REDIS_V2_COMMAND_TIMEOUT_MS =
process.env.NODE_ENV === "production" ? 1_000 : 10_000;
runtimeEnv.NODE_ENV === "production" ? 1_000 : 10_000;
export const getRedisV2ConnectionConfig = ({
cacheV2Url,

View File

@@ -1,13 +1,14 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { User } from "better-auth";
import { LoopsClient } from "loops";
import { logger } from "../logtail/logtailUtils.js";
const createLoopsCli = () => {
return new LoopsClient(process.env.LOOPS_API_KEY || "");
return new LoopsClient(runtimeEnv.LOOPS_API_KEY || "");
};
export const createLoopsContact = async (user: User) => {
if (!process.env.LOOPS_API_KEY) return;
if (!runtimeEnv.LOOPS_API_KEY) return;
try {
const email = user.email;

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { Resend } from "resend";
import { logger } from "../logtail/logtailUtils.js";
@@ -11,7 +12,7 @@ interface ResendEmailProps {
}
export const createResendCli = () => {
return new Resend(process.env.RESEND_API_KEY);
return new Resend(runtimeEnv.RESEND_API_KEY);
};
export const sendTextEmail = async ({

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { logger } from "../logtail/logtailUtils.js";
export function safeResend<T extends (...args: any[]) => any>({
@@ -8,7 +9,7 @@ export function safeResend<T extends (...args: any[]) => any>({
action: string;
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
return async (...args: Parameters<T>) => {
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
if (!runtimeEnv.RESEND_API_KEY || !runtimeEnv.RESEND_DOMAIN) {
logger.warn(
`RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`,
);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv } from "@autumn/shared";
import type { initRevenuecatCli } from "./initRevenuecatCli.js";
@@ -8,9 +9,9 @@ type RcCli = ReturnType<typeof initRevenuecatCli>;
* can reach a local tunnel); production uses BETTER_AUTH_URL.
*/
const getServerBaseUrl = (): string | undefined =>
process.env.NODE_ENV !== "production"
? process.env.NGROK_URL
: process.env.BETTER_AUTH_URL;
runtimeEnv.NODE_ENV !== "production"
? runtimeEnv.NGROK_URL
: runtimeEnv.BETTER_AUTH_URL;
export const getRevenuecatWebhookUrl = ({
orgId,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
CodeChallengeMethod,
generateCodeVerifier,
@@ -48,8 +49,8 @@ export const findMissingRcScopes = (grantedScopes: string[]): string[] => {
};
const getRcOAuthClient = () => {
const clientId = process.env.REVENUECAT_OAUTH_CLIENT_ID;
const clientSecret = process.env.REVENUECAT_OAUTH_CLIENT_SECRET;
const clientId = runtimeEnv.REVENUECAT_OAUTH_CLIENT_ID;
const clientSecret = runtimeEnv.REVENUECAT_OAUTH_CLIENT_SECRET;
if (!clientId || !clientSecret) {
throw new Error("RevenueCat OAuth client credentials not configured");
@@ -59,10 +60,10 @@ const getRcOAuthClient = () => {
};
export const getRcOAuthRedirectUri = () => {
let serverUrl = process.env.BETTER_AUTH_URL;
let serverUrl = runtimeEnv.BETTER_AUTH_URL;
if (process.env.NGROK_URL) {
serverUrl = process.env.NGROK_URL;
if (runtimeEnv.NGROK_URL) {
serverUrl = runtimeEnv.NGROK_URL;
}
return `${(serverUrl ?? "").replace(/\/+$/, "")}/revenuecat/oauth_callback`;

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import * as Sentry from "@sentry/bun";
import type { Context } from "hono";
import { Stripe } from "stripe";
@@ -122,7 +123,7 @@ export const handleStripeWebhookEvent = async (
}
if (
process.env.NODE_ENV === "development" &&
runtimeEnv.NODE_ENV === "development" &&
error instanceof Error &&
error.message.includes("No stripe account linked to organization")
) {

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { type AppEnv, ErrCode } from "@autumn/shared";
import Stripe from "stripe";
import RecaseError from "@/utils/errorUtils.js";
@@ -20,8 +21,7 @@ export const createWebhookEndpoint = async (
) => {
const stripe = new Stripe(apiKey);
const webhookBaseUrl =
process.env.STRIPE_WEBHOOK_URL || process.env.SERVER_URL;
const webhookBaseUrl = runtimeEnv.STRIPE_WEBHOOK_URL || runtimeEnv.SERVER_URL;
if (!webhookBaseUrl) {
throw new RecaseError({

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { getPrimaryRedis } from "@/external/redis/initRedis";
import { tryRedisRead, tryRedisWrite } from "@/utils/cacheUtils/cacheUtils";
@@ -15,7 +16,7 @@ export const setStripeSubscriptionLock = async ({
`sub:${stripeSubscriptionId}`,
JSON.stringify({ lockedAtMs }),
"EX",
process.env.NODE_ENV === "production" ? 60 : 3,
runtimeEnv.NODE_ENV === "production" ? 60 : 3,
),
primaryRedis,
);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { createStripeCli } from "@/external/connect/createStripeCli";
import { isStripeSubscriptionScheduleInLastPhase } from "@/external/stripe/subscriptionSchedules/utils/classifyStripeSubscriptionScheduleUtils";
import { stripeSubscriptionScheduleToPhaseIndex } from "@/external/stripe/subscriptionSchedules/utils/convertStripeSubscriptionScheduleUtils";
@@ -73,7 +74,7 @@ export const releaseScheduleIfLastPhase = async ({
return true;
} catch (error: unknown) {
if (error instanceof Error) {
if (process.env.NODE_ENV === "development") {
if (runtimeEnv.NODE_ENV === "development") {
logger.warn(
`[handleSchedulePhaseChanges] failed to release schedule: ${error.message}`,
);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
type AppEnv,
AuthType,
@@ -54,8 +55,8 @@ export const stripeConnectSeederMiddleware = async (
const signature = c.req.header("stripe-signature") || "";
const skipVerify =
process.env.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
process.env.NODE_ENV !== "production";
runtimeEnv.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
runtimeEnv.NODE_ENV !== "production";
let event: Stripe.Event;
if (skipVerify) {
@@ -78,7 +79,7 @@ export const stripeConnectSeederMiddleware = async (
);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
if (process.env.NODE_ENV !== "development") {
if (runtimeEnv.NODE_ENV !== "development") {
logger.warn(`Webhook verification error: ${message}`);
}
return c.json({ error: message }, 400);
@@ -114,7 +115,7 @@ export const stripeConnectSeederMiddleware = async (
return c.json({ error: "Failed to resolve org for Stripe webhook" }, 500);
}
if (process.env.NODE_ENV !== "development") {
if (runtimeEnv.NODE_ENV !== "development") {
logger.error(
`Account ID ${accountId} not linked to any org, skipping Stripe webhook`,
);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { tryCatch } from "@autumn/shared";
import type { Context, Next } from "hono";
import { redis } from "@/external/redis/initRedis";
@@ -16,7 +17,7 @@ export const stripeIdempotencyMiddleware = async (
c: Context<StripeWebhookHonoEnv>,
next: Next,
) => {
if (process.env.NODE_ENV === "development") {
if (runtimeEnv.NODE_ENV === "development") {
await next();
return;
}

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { type AppEnv, AuthType } from "@autumn/shared";
import type { Context, Next } from "hono";
import Stripe from "stripe";
@@ -54,8 +55,8 @@ export const stripeLegacySeederMiddleware = async (
const signature = c.req.header("stripe-signature") || "";
const skipVerify =
process.env.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
process.env.NODE_ENV !== "production";
runtimeEnv.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
runtimeEnv.NODE_ENV !== "production";
let event: Stripe.Event;
if (skipVerify) {

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { isSyncableEvent, processStripeSyncEvent } from "@autumn/stripe-sync";
import type { Context, Next } from "hono";
import { isStripeSyncEnabled } from "@/internal/misc/stripeSync/stripeSyncStore.js";
@@ -21,7 +22,7 @@ export const stripeSyncMiddleware = async (
if (!org || !stripeEvent) return;
if (
process.env.NODE_ENV === "production" &&
runtimeEnv.NODE_ENV === "production" &&
!isStripeSyncEnabled({ orgId: org.id, orgSlug: org.slug })
)
return;

View File

@@ -1,10 +1,11 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { createClient } from "@supabase/supabase-js";
export const createSupabaseClient = () => {
try {
return createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!,
runtimeEnv.SUPABASE_URL!,
runtimeEnv.SUPABASE_SERVICE_KEY!,
);
} catch (error) {
console.error("Error creating Supabase client:", error);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { AppEnv, Organization } from "@autumn/shared";
import * as Sentry from "@sentry/bun";
import { getSentryTags } from "@/external/sentry/sentryUtils.js";
@@ -53,7 +54,7 @@ export const sendSvixEvent = async ({
idempotencyKey?: string;
tags?: string[];
}) => {
if (!process.env.SVIX_API_KEY) return;
if (!runtimeEnv.SVIX_API_KEY) return;
const { org, env } = ctx;

View File

@@ -1,9 +1,10 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv, type Organization } from "@autumn/shared";
import { Svix } from "svix";
import { logger } from "../logtail/logtailUtils.js";
export const createSvixCli = () => {
return new Svix(process.env.SVIX_API_KEY as string);
return new Svix(runtimeEnv.SVIX_API_KEY as string);
};
export function safeSvix<T extends (...args: any[]) => any>({
@@ -14,7 +15,7 @@ export function safeSvix<T extends (...args: any[]) => any>({
action: string;
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
return async (...args: Parameters<T>) => {
if (!process.env.SVIX_API_KEY) {
if (!runtimeEnv.SVIX_API_KEY) {
logger.warn(`SVIX_API_KEY is not set, skipping ${action}`);
return;
}

View File

@@ -1,7 +1,8 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { type ClickHouseClient, createClient } from "@clickhouse/client";
const TINYBIRD_CLICKHOUSE_URL = process.env.TINYBIRD_US_EAST_CLICKHOUSE_URL;
const TINYBIRD_TOKEN = process.env.TINYBIRD_US_EAST_TOKEN;
const TINYBIRD_CLICKHOUSE_URL = runtimeEnv.TINYBIRD_US_EAST_CLICKHOUSE_URL;
const TINYBIRD_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN;
if (TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN) {
console.log(

View File

@@ -1,7 +1,8 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { createTinybirdApi } from "@tinybirdco/sdk";
const TINYBIRD_SECONDARY_API_URL = process.env.TINYBIRD_API_URL;
const TINYBIRD_SECONDARY_TOKEN = process.env.TINYBIRD_TOKEN;
const TINYBIRD_SECONDARY_API_URL = runtimeEnv.TINYBIRD_API_URL;
const TINYBIRD_SECONDARY_TOKEN = runtimeEnv.TINYBIRD_TOKEN;
/** Secondary Tinybird API client for dual-write safety net during region cutover.
* Reads from the legacy TINYBIRD_API_URL / TINYBIRD_TOKEN env vars (europe-west2

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
defineDatasource,
defineEndpoint,
@@ -9,8 +10,8 @@ import {
t,
} from "@tinybirdco/sdk";
const TINYBIRD_US_EAST_API_URL = process.env.TINYBIRD_US_EAST_API_URL;
const TINYBIRD_US_EAST_TOKEN = process.env.TINYBIRD_US_EAST_TOKEN;
const TINYBIRD_US_EAST_API_URL = runtimeEnv.TINYBIRD_US_EAST_API_URL;
const TINYBIRD_US_EAST_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN;
const migrationTinybirdConfig =
TINYBIRD_US_EAST_API_URL && TINYBIRD_US_EAST_TOKEN

View File

@@ -1,8 +1,9 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, RecaseError } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
const TINYBIRD_API_URL = process.env.TINYBIRD_US_EAST_API_URL;
const TINYBIRD_TOKEN = process.env.TINYBIRD_US_EAST_TOKEN;
const TINYBIRD_API_URL = runtimeEnv.TINYBIRD_US_EAST_API_URL;
const TINYBIRD_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN;
export type TinybirdConfig = {
baseUrl: string;

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv, type Organization } from "@autumn/shared";
import { createRemoteJWKSet, jwtVerify } from "jose";
import { JWTExpired, JWTInvalid } from "jose/errors";
@@ -42,7 +43,7 @@ const synthesizeTestClaims = ({
env: AppEnv;
testOptions?: VercelOidcTestOptions;
}): OidcClaims | null => {
if (process.env.NODE_ENV === "production") return null;
if (runtimeEnv.NODE_ENV === "production") return null;
if (testOptions?.allowVercelTestOidc !== true) return null;
if (!token.startsWith(TEST_OIDC_PREFIX)) return null;

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
export type VercelSdkTestOptions = {
mockVercelApi?: boolean;
};
@@ -8,9 +9,9 @@ export type VercelSdkTestOptions = {
export const getVercelSdkServerURL = (
testOptions?: VercelSdkTestOptions,
): string | undefined => {
if (process.env.NODE_ENV === "production") return undefined;
if (runtimeEnv.NODE_ENV === "production") return undefined;
if (testOptions?.mockVercelApi !== true) return undefined;
const base = process.env.BETTER_AUTH_URL;
const base = runtimeEnv.BETTER_AUTH_URL;
if (!base) return undefined;
return `${base.replace(/\/$/, "")}/__test/vercel/api`;
};

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
ApiVersionClass,
AppEnv,
@@ -79,7 +80,7 @@ export const baseMiddleware = async (c: Context<HonoEnv>, next: Next) => {
entity_id: entityId,
user_agent: c.req.header("user-agent"),
ip_address: c.req.header("x-forwarded-for"),
region: process.env.AWS_REGION,
region: runtimeEnv.AWS_REGION,
query: c.req.query(),
body: redactSensitiveRequestBody({ body }),
@@ -129,7 +130,7 @@ export const baseMiddleware = async (c: Context<HonoEnv>, next: Next) => {
useReplica: c.req.header("x-use-replica") === "true",
mockVercelApi: c.req.header("x-mock-vercel-api") === "true",
allowVercelTestOidc:
process.env.NODE_ENV !== "production" &&
runtimeEnv.NODE_ENV !== "production" &&
c.req.header("x-allow-vercel-test-oidc") === "true",
},
});

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, RecaseError } from "@autumn/shared";
import type { Context } from "hono";
import type { ContentfulStatusCode } from "hono/utils/http-status";
@@ -47,7 +48,7 @@ const ROUTE_SPECIFIC_RULES: Array<{
match: (err: Error) =>
err.message.includes(
"STRIPE_WEBHOOK_SECRET env variable is not set (live)",
) && process.env.NODE_ENV === "development",
) && runtimeEnv.NODE_ENV === "development",
statusCode: 500,
},
];

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { Context, Env, Next } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import {
@@ -24,9 +25,9 @@ export const rateLimitMiddleware = async (c: Context<HonoEnv>, next: Next) => {
if (
rateLimitType === RateLimitType.Attach &&
(process.env.NODE_ENV === "development" ||
process.env.NODE_ENV === "test") &&
ctx.org?.id === process.env.TESTS_ORG_ID
(runtimeEnv.NODE_ENV === "development" ||
runtimeEnv.NODE_ENV === "test") &&
ctx.org?.id === runtimeEnv.TESTS_ORG_ID
) {
return await next();
}

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import chalk from "chalk";
import type { Context } from "hono";
import type { AutumnContext, HonoEnv } from "@/honoUtils/HonoEnv.js";
@@ -14,7 +15,7 @@ const HIGH_VOLUME_SUCCESS_ROUTES = new Set<string>([
]);
const SUCCESS_REQUEST_LOG_SAMPLE_RATE = Number.parseFloat(
process.env.AXIOM_SUCCESS_REQUEST_LOG_SAMPLE_RATE ?? "0",
runtimeEnv.AXIOM_SUCCESS_REQUEST_LOG_SAMPLE_RATE ?? "0",
);
const shouldSampleSuccessLog = () =>
@@ -80,7 +81,7 @@ export const logRequestResult = async ({
if (
Object.keys(ctx.extraLogs).length > 0 &&
process.env.NODE_ENV === "development"
runtimeEnv.NODE_ENV === "development"
) {
const maskedLogs = maskExtraLogs(ctx.extraLogs);
ctx.logger.debug(`EXTRA LOGS: ${JSON.stringify(maskedLogs, null, 2)}`);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { AuthType } from "@autumn/shared";
import type { Context, Next } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
@@ -49,7 +50,7 @@ export const responseFilterMiddleware = async (
const ctx = c.get("ctx");
if (ctx?.authType === AuthType.Dashboard) return;
const isNonProd =
process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
runtimeEnv.NODE_ENV === "development" || runtimeEnv.NODE_ENV === "test";
if (isNonProd && ctx?.testOptions?.keepInternalFields === true) return;
// Only process JSON responses

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { context, trace } from "@opentelemetry/api";
import type { MiddlewareHandler } from "hono";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
@@ -28,7 +29,7 @@ export const traceEnrichMiddleware: MiddlewareHandler = async (c, next) => {
user_id: ctx.userId || undefined,
auth_type: ctx.authType,
api_version: ctx.apiVersion?.semver,
region: process.env.AWS_REGION,
region: runtimeEnv.AWS_REGION,
full_subject_rollout_enabled: ctx.org
? isFullSubjectRolloutEnabled({ ctx })
: undefined,

View File

@@ -92,6 +92,7 @@ export type RequestContext = {
export type AutumnContext = RequestContext;
export type HonoEnv = {
Bindings: Env;
Variables: {
ctx: AutumnContext;
validated: boolean;

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { timingSafeEqual } from "node:crypto";
import type { Context } from "hono";
import { clientCritical } from "@/db/initDrizzle.js";
@@ -7,7 +8,7 @@ import { getRedisV2Availability } from "@/external/redis/initUtils/redisV2Availa
import type { HonoEnv } from "./HonoEnv.js";
const POSTGRES_TIMEOUT_MS = 1_000;
const READY_CHECK_TOKEN = process.env.READY_CHECK_TOKEN?.trim();
const READY_CHECK_TOKEN = runtimeEnv.READY_CHECK_TOKEN?.trim();
const checkPostgresReady = async () => {
try {

View File

@@ -1,10 +1,14 @@
import { createHonoApp } from "./initHono.js";
import { setRuntimeEnv } from "@/utils/envUtils.js";
import type { ExecutionContext } from "hono";
import type { createHonoApp } from "./initHono.js";
const app = createHonoApp();
let app: ReturnType<typeof createHonoApp> | null = null;
export default {
fetch(request: Request, env: unknown, executionCtx: ExecutionContext) {
async fetch(request: Request, env: Env, executionCtx: ExecutionContext) {
setRuntimeEnv(env);
const { createHonoApp } = await import("./initHono.js");
app ??= createHonoApp(env);
return app.fetch(request, env, executionCtx);
},
};

View File

@@ -1,4 +1,5 @@
// Sentry + OpenTelemetry must be imported before any application code
import { runtimeEnv } from "@/utils/envUtils.js";
await import("./sentry.js");
import cluster from "node:cluster";
@@ -55,7 +56,7 @@ let shuttingDown = false;
const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
logger.info(getRedactedDatabaseUrls(), "DB URLs");
const app = createHonoApp();
const app = createHonoApp(runtimeEnv as unknown as Env);
initPgHealthMonitor({ client: clientCritical });
startPgPoolMonitor();
@@ -72,8 +73,8 @@ const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
startRedisMonitor();
startRedisV2Monitor();
const PORT = process.env.SERVER_PORT
? Number.parseInt(process.env.SERVER_PORT)
const PORT = runtimeEnv.SERVER_PORT
? Number.parseInt(runtimeEnv.SERVER_PORT)
: 8080;
const requestListener = getRequestListener(app.fetch);
@@ -94,7 +95,7 @@ const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
});
};
if (process.env.NODE_ENV === "development") {
if (runtimeEnv.NODE_ENV === "development") {
registerFatalErrorHandlers();
await init({ startupStartedAt: Date.now() });
registerShutdownHandlers();

View File

@@ -15,8 +15,8 @@ import { handleReadyCheck } from "./honoUtils/handleReadyCheck.js";
import { handleListAuthOrganizations } from "./internal/auth/handleListAuthOrganizations.js";
import { oauthRouter } from "./internal/auth/oauth/oauthRouter.js";
import { cliRouter } from "./internal/dev/cli/cliRouter.js";
import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleOAuthCallback.js";
import { handleRevenueCatOAuthCallback } from "./internal/orgs/handlers/revenueCatHandlers/handleRevenueCatOAuthCallback.js";
import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleOAuthCallback.js";
import { apiRouter } from "./routers/apiRouter.js";
import { createChatProxyRouter } from "./routers/chatProxyRouter.js";
import { internalRouter } from "./routers/internalRouter.js";
@@ -48,7 +48,7 @@ const ALLOWED_HEADERS = [
"User-Agent", // Required for better-auth v1.4.0+ compatibility with Safari/Zen browser
];
export const createHonoApp = () => {
export const createHonoApp = (env?: Env) => {
const app = new Hono<HonoEnv>();
app.route("", createChatProxyRouter());
@@ -100,7 +100,7 @@ export const createHonoApp = () => {
// Add Render region identifier header for load balancer verification
app.use("*", async (c, next) => {
await next();
c.header("x-region", process.env.AWS_REGION);
c.header("x-region", c.env.AWS_REGION);
});
// Webhook routes
@@ -111,7 +111,7 @@ export const createHonoApp = () => {
// Vercel SDK test mock — mounted in dev/test, used only when
// `ctx.testOptions.mockVercelApi` points the SDK at this route.
if (process.env.NODE_ENV !== "production") {
if ((env?.NODE_ENV as string | undefined) !== "production") {
app.route("/__test/vercel/api", vercelTestApiRouter);
}

View File

@@ -1,4 +1,4 @@
import "dotenv/config";
import { runtimeEnv } from "@/utils/envUtils.js";
import { DiagConsoleLogger, DiagLogLevel, diag } from "@opentelemetry/api";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
@@ -14,14 +14,14 @@ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);
let sdk: NodeSDK | null = null;
if (process.env.AXIOM_TOKEN) {
if (runtimeEnv.AXIOM_TOKEN) {
// NodeSDK reads OTEL_SERVICE_NAME to set the service resource attribute
process.env.OTEL_SERVICE_NAME = "autumn-server";
runtimeEnv.OTEL_SERVICE_NAME = "autumn-server";
const traceExporter = new OTLPTraceExporter({
url: "https://api.axiom.co/v1/traces",
headers: {
Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
Authorization: `Bearer ${runtimeEnv.AXIOM_TOKEN}`,
"X-Axiom-Dataset": "otel",
},
});
@@ -30,18 +30,18 @@ if (process.env.AXIOM_TOKEN) {
// auto-add a BatchSpanProcessor for `traceExporter` when `spanProcessors`
// is set. We must wire the exporter processor explicitly.
// Dev: short 1s flush for fast feedback. Prod: default 5s for throughput.
const isDev = process.env.NODE_ENV !== "production";
const isDev = runtimeEnv.NODE_ENV !== "production";
const exportProcessor = new BatchSpanProcessor(traceExporter, {
scheduledDelayMillis: isDev ? 1000 : 5000,
});
const filteredExportProcessor = new FilteringSpanProcessor(exportProcessor);
const metricReader = process.env.AXIOM_METRICS_DATASET
const metricReader = runtimeEnv.AXIOM_METRICS_DATASET
? new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: "https://api.axiom.co/v1/metrics",
headers: {
Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
"x-axiom-metrics-dataset": process.env.AXIOM_METRICS_DATASET,
Authorization: `Bearer ${runtimeEnv.AXIOM_TOKEN}`,
"x-axiom-metrics-dataset": runtimeEnv.AXIOM_METRICS_DATASET,
},
}),
exportIntervalMillis: 60_000,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import crypto, { randomUUID } from "node:crypto";
import { stripOAuthTokenPrefix } from "@autumn/auth";
import {
@@ -82,7 +83,7 @@ const getOrgSummary = async ({
const decryptChatCredentialToken = ({ token }: { token: string }) => {
const key = crypto
.createHash("sha256")
.update(process.env.ENCRYPTION_PASSWORD ?? "")
.update(runtimeEnv.ENCRYPTION_PASSWORD ?? "")
.digest();
const buffer = Buffer.from(token, "base64");
if (buffer[0] !== 1) throw new Error("Unsupported encrypted payload");

View File

@@ -1,9 +1,10 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, RecaseError, Scopes } from "@autumn/shared";
import { registerMcpOAuthClient } from "@/internal/auth/actions/index.js";
import { createRoute } from "../../honoMiddlewares/routeHandler";
const getClientUrl = () =>
(process.env.CLIENT_URL || "http://localhost:3000").replace(/\/+$/, "");
(runtimeEnv.CLIENT_URL || "http://localhost:3000").replace(/\/+$/, "");
const getSlackMcpRedirectUris = () => {
const clientUrl = getClientUrl();

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, type EventInsert, events, RecaseError } from "@autumn/shared";
import { and, desc, eq } from "drizzle-orm";
import { StatusCodes } from "http-status-codes";
@@ -14,7 +15,7 @@ export class EventService {
event: EventInsert | EventInsert[];
logger?: Logger;
}) {
if (process.env.NODE_ENV !== "development") return;
if (runtimeEnv.NODE_ENV !== "development") return;
try {
const results = await db
.insert(events)
@@ -48,7 +49,7 @@ export class EventService {
env: string;
limit?: number;
}) {
if (process.env.NODE_ENV === "production") return [];
if (runtimeEnv.NODE_ENV === "production") return [];
const results = await db
.select({
id: events.id,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { oauthClientRepo } from "../repos/index.js";
@@ -5,7 +6,7 @@ const ATMN_OAUTH_CLIENT_NAMES = new Set(["atmn", "autumn cli"]);
const configuredAtmnClientIds = () =>
new Set(
(process.env.ATMN_OAUTH_CLIENT_IDS ?? "")
(runtimeEnv.ATMN_OAUTH_CLIENT_IDS ?? "")
.split(",")
.map((id) => id.trim())
.filter(Boolean),

View File

@@ -1,10 +1,11 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { MCP_CLIENT_KIND } from "@autumn/auth/oauth";
import type { Context } from "hono";
import { type DrizzleCli, db } from "@/db/initDrizzle.js";
import { auth } from "@/utils/auth.js";
import { oauthClientRepo } from "../repos/index.js";
const INTERNAL_MCP_CLIENT_ID = process.env.INTERNAL_MCP_OAUTH_CLIENT_ID;
const INTERNAL_MCP_CLIENT_ID = runtimeEnv.INTERNAL_MCP_OAUTH_CLIENT_ID;
const INTERNAL_MCP_CLIENT_NAME = "Autumn internal-mcp";
const INTERNAL_MCP_CLIENT_NAME_NORMALIZED =
INTERNAL_MCP_CLIENT_NAME.toLowerCase();

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { stripOAuthTokenPrefix } from "@autumn/auth";
import {
AppEnv,
@@ -19,7 +20,7 @@ import { isAtmnOAuthClientId } from "./atmnOAuthClients.js";
import { rotateOAuthConsentApiKey } from "./oauthConsentApiKey.js";
const getOAuthIssuer = () =>
`${process.env.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`;
`${runtimeEnv.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`;
const verifyResourceAccessToken = async ({
accessToken,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
oauthProviderAuthServerMetadata,
oauthProviderOpenIdConfigMetadata,
@@ -22,7 +23,7 @@ const getClientLookupRateLimitKey = (c: Context<HonoEnv>) =>
const oauthClientLookupLimiter = rateLimiter<HonoEnv>({
windowMs: 60 * 1000,
limit: process.env.NODE_ENV === "development" ? 1000 : 60,
limit: runtimeEnv.NODE_ENV === "development" ? 1000 : 60,
standardHeaders: "draft-6",
keyGenerator: getClientLookupRateLimitKey,
});

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, RecaseError, type TrackParams } from "@autumn/shared";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { queueTrack } from "./utils/queueTrack.js";
@@ -12,7 +13,7 @@ export const runAsyncTrack = async ({
ctx: AutumnContext;
body: TrackParams;
}): Promise<void> => {
const queueUrl = process.env.TRACK_ASYNC_SQS_QUEUE_URL;
const queueUrl = runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL;
if (!queueUrl) {
ctx.logger.error(
"[track] async=true requested but TRACK_ASYNC_SQS_QUEUE_URL is unset",

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { type BatchTrackParams, ErrCode, RecaseError } from "@autumn/shared";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { JobName } from "@/queue/JobName.js";
@@ -15,7 +16,7 @@ export const runBatchTrack = async ({
ctx: AutumnContext;
body: BatchTrackParams;
}): Promise<void> => {
const queueUrl = process.env.TRACK_ASYNC_SQS_QUEUE_URL;
const queueUrl = runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL;
if (!queueUrl) {
ctx.logger.error(
"[track] batch track requested but TRACK_ASYNC_SQS_QUEUE_URL is unset",

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { TrackParams } from "@autumn/shared";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { JobName } from "@/queue/JobName.js";
@@ -17,7 +18,7 @@ export const queueTrack = async ({
messageDeduplicationId?: string;
}) => {
try {
const resolvedQueueUrl = queueUrl ?? process.env.TRACK_SQS_QUEUE_URL;
const resolvedQueueUrl = queueUrl ?? runtimeEnv.TRACK_SQS_QUEUE_URL;
if (!resolvedQueueUrl) {
ctx.logger.warn(
"[track] Redis unavailable and TRACK_SQS_QUEUE_URL is unset; falling back to synchronous track",

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
type FullCusEntWithFullCusProduct,
type FullSubject,
@@ -232,7 +233,7 @@ export const executeRedisDeductionV2 = async ({
}
: null,
unwind_value: unwindValue ?? null,
debug: process.env.NODE_ENV !== "production",
debug: runtimeEnv.NODE_ENV !== "production",
};
const targetRedis = redisInstance ?? ctx.redisV2;

View File

@@ -1,8 +1,9 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { AppEnv } from "@autumn/shared";
import { JobName } from "@/queue/JobName.js";
export const REFRESH_ENTITY_AGGREGATE_DEDUP_BUCKET_MS =
process.env.NODE_ENV === "development" ? 1000 : 5000;
runtimeEnv.NODE_ENV === "development" ? 1000 : 5000;
/**
* Buffer added after the bucket boundary so the trailing enqueue fires *after*

View File

@@ -1,4 +1,9 @@
import { type AttachResponseV1, AttachResponseV1Schema, Scopes } from "@autumn/shared";
import { runtimeEnv } from "@/utils/envUtils.js";
import {
type AttachResponseV1,
AttachResponseV1Schema,
Scopes,
} from "@autumn/shared";
import { AttachBodyV0Schema } from "../../../../../shared/api/billing/attach/prevVersions/attachBodyV0";
import {
AffectedResource,
@@ -21,7 +26,7 @@ export const handleAttach = createRoute({
resource: AffectedResource.Attach,
lock:
process.env.NODE_ENV !== "development"
runtimeEnv.NODE_ENV !== "development"
? {
ttlMs: 60000,
errorMessage:

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
AffectedResource,
ApiVersion,
@@ -19,7 +20,7 @@ export const handleAttachV2 = createRoute({
},
resource: AffectedResource.Attach,
lock:
process.env.NODE_ENV !== "development"
runtimeEnv.NODE_ENV !== "development"
? {
ttlMs: 120000,
errorMessage:

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
CreateScheduleParamsV0Schema,
type CreateScheduleResponse,
@@ -13,7 +14,7 @@ export const handleCreateSchedule = createRoute({
body: CreateScheduleParamsV0Schema,
lock:
process.env.NODE_ENV !== "development"
runtimeEnv.NODE_ENV !== "development"
? {
ttlMs: 120000,
errorMessage:

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
AffectedResource,
InternalError,
@@ -14,7 +15,7 @@ export const handleMultiAttach = createRoute({
body: MultiAttachParamsV0Schema,
resource: AffectedResource.MultiAttach,
lock:
process.env.NODE_ENV !== "development"
runtimeEnv.NODE_ENV !== "development"
? {
ttlMs: 120000,
errorMessage:

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
AffectedResource,
ApiVersion,
@@ -19,7 +20,7 @@ export const handleUpdateSubscription = createRoute({
},
resource: AffectedResource.ApiSubscriptionUpdate,
lock:
process.env.NODE_ENV !== "development"
runtimeEnv.NODE_ENV !== "development"
? {
ttlMs: 120000,
errorMessage:

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { BillingContext, StripeSubscriptionAction } from "@autumn/shared";
import { AppEnv, ErrCode, RecaseError } from "@autumn/shared";
import { stripeSubscriptionToApplication } from "@/external/stripe/subscriptions/utils/convertStripeSubscription";
@@ -6,8 +7,8 @@ import { isStripeConnected } from "@/internal/orgs/orgUtils";
const expectedStripeApplicationId = ({ ctx }: { ctx: AutumnContext }) =>
ctx.env === AppEnv.Live
? process.env.STRIPE_LIVE_CLIENT_ID
: process.env.STRIPE_SANDBOX_CLIENT_ID;
? runtimeEnv.STRIPE_LIVE_CLIENT_ID
: runtimeEnv.STRIPE_SANDBOX_CLIENT_ID;
const shouldValidateStripeApplicationOwnership = ({
ctx,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, RecaseError } from "@autumn/shared";
export const slackProvider = "slack" as const;
@@ -31,7 +32,7 @@ export const getMissingSlackScopes = (scopes: string[]) => {
};
export const getRequiredChatEnv = (key: string) => {
const value = process.env[key];
const value = runtimeEnv[key];
if (value) return value;
throw new RecaseError({
@@ -42,20 +43,20 @@ export const getRequiredChatEnv = (key: string) => {
};
export const getChatStateSecret = () =>
process.env.CHAT_STATE_SECRET ??
process.env.SLACK_STATE_SECRET ??
process.env.BETTER_AUTH_SECRET ??
runtimeEnv.CHAT_STATE_SECRET ??
runtimeEnv.SLACK_STATE_SECRET ??
runtimeEnv.BETTER_AUTH_SECRET ??
getRequiredChatEnv("ENCRYPTION_PASSWORD");
export const createSlackInstallUrl = (state: string) => {
const scope = process.env.SLACK_BOT_SCOPES ?? defaultSlackScopes.join(",");
const scope = runtimeEnv.SLACK_BOT_SCOPES ?? defaultSlackScopes.join(",");
const params = new URLSearchParams({
client_id: getRequiredChatEnv("SLACK_CLIENT_ID"),
scope,
state,
});
if (process.env.SLACK_REDIRECT_URI) {
params.set("redirect_uri", process.env.SLACK_REDIRECT_URI);
if (runtimeEnv.SLACK_REDIRECT_URI) {
params.set("redirect_uri", runtimeEnv.SLACK_REDIRECT_URI);
}
return `https://slack.com/oauth/v2/authorize?${params}`;
};

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
AffectedResource,
type Checkout,
@@ -24,7 +25,7 @@ export const handleConfirmCheckout = createRoute({
resource: AffectedResource.Attach,
body: ConfirmCheckoutParamsSchema,
lock:
process.env.NODE_ENV !== "development"
runtimeEnv.NODE_ENV !== "development"
? {
ttlMs: 120000,
errorMessage:

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { UpdateSubscriptionV1Params } from "@autumn/shared";
import { Scopes } from "@autumn/shared";
import { createRoute } from "@/honoMiddlewares/routeHandler";
@@ -16,7 +17,7 @@ import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumn
export const handleCancelV2 = createRoute({
scopes: [Scopes.Billing.Write],
lock:
process.env.NODE_ENV !== "development"
runtimeEnv.NODE_ENV !== "development"
? {
ttlMs: 120000,
errorMessage:

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import type { AppEnv } from "@autumn/shared";
import type { Redis } from "ioredis";
import {
@@ -18,7 +19,7 @@ type CustomerToDelete = {
customerId: string;
};
const isProductionNode = process.env.NODE_ENV === "production";
const isProductionNode = runtimeEnv.NODE_ENV === "production";
/**
* Per org: all keys share `{orgId}` so Redis Cluster stays in one slot per pipeline.

View File

@@ -1,2 +1,3 @@
export const FROM_AUTUMN = `Autumn <hey@${process.env.RESEND_DOMAIN}>`;
export const FROM_AYUSH = `Ayush <ayush@${process.env.RESEND_DOMAIN}>`;
import { runtimeEnv } from "@/utils/envUtils.js";
export const FROM_AUTUMN = `Autumn <hey@${runtimeEnv.RESEND_DOMAIN}>`;
export const FROM_AYUSH = `Ayush <ayush@${runtimeEnv.RESEND_DOMAIN}>`;

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { logger } from "@/external/logtail/logtailUtils.js";
import { sendTextEmail } from "@/external/resend/resendUtils.js";
import { safeResend } from "@/external/resend/safeResend.js";
@@ -8,7 +9,7 @@ const getInvitationEmailBody = ({ orgName }: { orgName: string }) => {
Click the link below to create an account / sign in to Autumn and accept the invitation.
${process.env.CLIENT_URL}/sign-in
${runtimeEnv.CLIENT_URL}/sign-in
`;
};

View File

@@ -1,10 +1,11 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { logger } from "@/external/logtail/logtailUtils.js";
import { createResendCli } from "@/external/resend/resendUtils.js";
import { FROM_AUTUMN } from "./constants.js";
import OTPEmail from "./OTPEmail.js";
const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => {
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
if (!runtimeEnv.RESEND_API_KEY || !runtimeEnv.RESEND_DOMAIN) {
logger.warn(`RESEND NOT SET UP, SIGN IN OTP: ${otp}`);
return;
}

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import {
type ApiInvoiceV1,
type Customer,
@@ -43,7 +44,7 @@ export const processInvoice = ({
currency: invoice.currency,
created_at: invoice.created_at,
hosted_invoice_url: isStripe
? `${process.env.BETTER_AUTH_URL}/invoices/hosted_invoice_url/${invoice.id}`
? `${runtimeEnv.BETTER_AUTH_URL}/invoices/hosted_invoice_url/${invoice.id}`
: null,
// hosted_invoice_url: invoice.hosted_invoice_url,
// items: withItems

View File

@@ -1,7 +1,3 @@
import dotenv from "dotenv";
dotenv.config();
import {
type Customer,
type FullProduct,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, RecaseError, Scopes } from "@autumn/shared";
import { auth } from "@trigger.dev/sdk/v3";
import { z } from "zod/v4";
@@ -72,7 +73,7 @@ export const handleRunMigration = createRoute({
});
}
const isDev = process.env.NODE_ENV === "development";
const isDev = runtimeEnv.NODE_ENV === "development";
const { migrationRunId, triggerRunId } = await withMigrationRunClaim({
ctx,
migration,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import chalk from "chalk";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { addExtrasToLogs } from "@/utils/logging/addContextToLogs.js";
@@ -62,7 +63,7 @@ export const logMigrateCustomerResult = ({
if (
Object.keys(ctx.extraLogs).length > 0 &&
process.env.NODE_ENV === "development"
runtimeEnv.NODE_ENV === "development"
) {
const maskedLogs = maskExtraLogs(ctx.extraLogs);
ctx.logger.debug(`EXTRA LOGS: ${JSON.stringify(maskedLogs, null, 2)}`);

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, ms } from "@autumn/shared";
import type { S3Client } from "@aws-sdk/client-s3";
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
@@ -26,7 +27,7 @@ export const createEdgeConfigStore = <T>({
s3Key,
schema,
defaultValue,
pollIntervalMs = process.env.NODE_ENV === "development"
pollIntervalMs = runtimeEnv.NODE_ENV === "development"
? ms.seconds(1)
: ms.seconds(10),
s3Client: injectedS3Client,

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { z } from "zod/v4";
import { Scopes } from "@autumn/shared";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
@@ -16,7 +17,7 @@ export const handleSubmitFeedback = createRoute({
const userEmail = ctx.user?.email ?? "Unknown user";
const orgSlug = ctx.org?.slug ?? "Unknown org";
const webhookUrl = process.env.DISCORD_FEEDBACK_WEBHOOK;
const webhookUrl = runtimeEnv.DISCORD_FEEDBACK_WEBHOOK;
if (!webhookUrl) {
console.warn("DISCORD_FEEDBACK_WEBHOOK not configured");
return c.json({ success: true });

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { createAnthropic } from "@ai-sdk/anthropic";
import { type AgentPricingConfig, InternalError } from "@autumn/shared";
import { withTracing } from "@posthog/ai";
@@ -12,12 +13,12 @@ import { OrganisationConfigurationSchema } from "./pricingAgentSchemas.js";
// PostHog client singleton
let phClient: PostHog | null = null;
const getPostHogClient = (): PostHog | null => {
if (!process.env.POSTHOG_API_KEY) {
if (!runtimeEnv.POSTHOG_API_KEY) {
return null;
}
if (!phClient) {
phClient = new PostHog(process.env.POSTHOG_API_KEY, {
host: process.env.POSTHOG_HOST || "https://us.i.posthog.com",
phClient = new PostHog(runtimeEnv.POSTHOG_API_KEY, {
host: runtimeEnv.POSTHOG_HOST || "https://us.i.posthog.com",
});
}
return phClient;
@@ -122,7 +123,7 @@ pricingAgentRouter.post("/chat", async (c) => {
} = await c.req.json();
const ctx = c.var.ctx;
if (!process.env.ANTHROPIC_API_KEY) {
if (!runtimeEnv.ANTHROPIC_API_KEY) {
throw new InternalError({
message: "ANTHROPIC_API_KEY not configured",
code: "anthropic_not_configured",
@@ -150,7 +151,7 @@ When the user asks to make changes, modify this existing configuration rather th
// Create Anthropic client and optionally wrap with PostHog tracing
const anthropicClient = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
apiKey: runtimeEnv.ANTHROPIC_API_KEY,
});
const baseModel = anthropicClient("claude-opus-4-5");

View File

@@ -1,3 +1,4 @@
import { runtimeEnv } from "@/utils/envUtils.js";
import { ApiVersion, ApiVersionClass } from "@autumn/shared";
import type { Context } from "hono";
import { matchRoute } from "../../../honoMiddlewares/middlewareUtils";
@@ -224,7 +225,7 @@ export const resolveRateLimit = ({
export const RATE_LIMIT_CONFIGS: Record<RateLimitType, RateLimitConfig> = {
[RateLimitType.General]: {
name: "general",
limit: process.env.NODE_ENV === "development" ? 1000 : 25,
limit: runtimeEnv.NODE_ENV === "development" ? 1000 : 25,
windowMs: 1000,
notInRedis: false,
scope: RateLimitScope.Org,

Some files were not shown because too many files have changed in this diff Show More