refactor: pass env explicitly across all remaining request-path modules
- Remove last runtimeEnv import from processMessage.ts - Add env: Env param to all functions with 'Cannot find name env' errors - Convert initRedisV2 to lazy initialization (ensureRedisV2 + Proxy) - Add Bindings: Env to StripeWebhookHonoEnv - Fix 12+ files across external/, internal/, queue/ modules - Scanning test passes: 1/1 (runtime-env-imports.test.ts)
This commit is contained in:
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -1,8 +1,12 @@
|
|||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"python.defaultInterpreterPath": "others/python-test/.venv/bin/python",
|
"python.defaultInterpreterPath": "others/python-test/.venv/bin/python",
|
||||||
"python.analysis.extraPaths": ["others/python-sdk/src"],
|
"python.analysis.extraPaths": [
|
||||||
"python.autoComplete.extraPaths": ["others/python-sdk/src"],
|
"others/python-sdk/src"
|
||||||
|
],
|
||||||
|
"python.autoComplete.extraPaths": [
|
||||||
|
"others/python-sdk/src"
|
||||||
|
],
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnPaste": true,
|
"editor.formatOnPaste": true,
|
||||||
@@ -12,7 +16,7 @@
|
|||||||
"source.organizeImports.biome": "explicit"
|
"source.organizeImports.biome": "explicit"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
},
|
},
|
||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
|||||||
72
scripts/fix-env-threading.ts
Normal file
72
scripts/fix-env-threading.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Bulk migration: runtimeEnv → env, global logger → createLogger(env)
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { globSync } from "bun:fs";
|
||||||
|
|
||||||
|
const SRC = "/Volumes/sker/autumn/server/src";
|
||||||
|
|
||||||
|
const files = globSync(`${SRC}/**/*.ts`).filter(
|
||||||
|
(f) => !f.includes("node_modules") && !f.includes(".test."),
|
||||||
|
);
|
||||||
|
|
||||||
|
let changed = 0;
|
||||||
|
|
||||||
|
for (const f of files) {
|
||||||
|
let content = readFileSync(f, "utf-8");
|
||||||
|
const original = content;
|
||||||
|
let modified = false;
|
||||||
|
|
||||||
|
// ── Remove runtimeEnv imports ──
|
||||||
|
if (content.match(/import\s+\{[^}]*runtimeEnv[^}]*\}\s+from\s+["'][^"']*envUtils[^"']*["'];?/)) {
|
||||||
|
content = content.replace(
|
||||||
|
/^import\s+\{[^}]*runtimeEnv[^}]*\}\s+from\s+["'][^"']*envUtils[^"']*["'];?\s*\n/gm,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Replace runtimeEnv.XXX → env.XXX ──
|
||||||
|
if (content.includes("runtimeEnv.")) {
|
||||||
|
content = content.replace(/runtimeEnv\./g, "env.");
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Replace import { logger } with import { createLogger } ──
|
||||||
|
if (content.match(/import\s+\{[^}]*\blogger\b[^}]*\}\s+from\s+["'][^"']*logtailUtils[^"']*["'];?/)) {
|
||||||
|
// Add createLogger to the existing import, or create new import
|
||||||
|
if (content.includes("import { createLogger }")) {
|
||||||
|
// Already has createLogger, just remove logger
|
||||||
|
content = content.replace(
|
||||||
|
/import\s+\{([^}]*)logger([^}]*)\}\s+from\s+["'][^"']*logtailUtils[^"']*["'];?/g,
|
||||||
|
(_, p1, p2) => {
|
||||||
|
const parts = [p1.trim(), p2.trim()].filter(Boolean).join(", ");
|
||||||
|
return parts
|
||||||
|
? `import { ${parts} } from "@/external/logtail/logtailUtils.js";`
|
||||||
|
: 'import { createLogger } from "@/external/logtail/logtailUtils.js";';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = content.replace(
|
||||||
|
/import\s+\{([^}]*)logger\b([^}]*)\}\s+from\s+["'][^"']*logtailUtils[^"']*["'];?/g,
|
||||||
|
(_, p1, p2) => {
|
||||||
|
const hasOther = (p1 + p2).replace(/\s+/g, "").length > 0;
|
||||||
|
if (hasOther) {
|
||||||
|
return `import { createLogger, ${[p1, p2].map(s => s.trim()).filter(Boolean).join(", ")} } from "@/external/logtail/logtailUtils.js";`;
|
||||||
|
}
|
||||||
|
return 'import { createLogger } from "@/external/logtail/logtailUtils.js";';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified) {
|
||||||
|
writeFileSync(f, content);
|
||||||
|
changed++;
|
||||||
|
console.log(`✓ ${f.replace(SRC, "")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nChanged ${changed} files.`);
|
||||||
88
scripts/fix-logger.ts
Normal file
88
scripts/fix-logger.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Bulk fix: adds const logger = createLogger(env) to files that import
|
||||||
|
* createLogger and use logger.X but don't define logger locally.
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const FILES = process.argv.slice(2);
|
||||||
|
|
||||||
|
function addLoggerLine(content: string, filename: string): string {
|
||||||
|
// Check if env is in scope (function param or const env = ...)
|
||||||
|
const hasEnv = /\benv\s*[:=]\s*\w+\s*Env\b/.test(content) ||
|
||||||
|
/\bc\.env\b/.test(content) ||
|
||||||
|
/\benv\./.test(content);
|
||||||
|
|
||||||
|
if (!hasEnv) {
|
||||||
|
console.log(` SKIP (no env in scope): ${filename}`);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add const logger = createLogger(env); before first logger. usage
|
||||||
|
// Find the function body where logger is used
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const result: string[] = [];
|
||||||
|
let inLoggerFunc = false;
|
||||||
|
let braceDepth = 0;
|
||||||
|
let foundLine: number | null = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
|
||||||
|
// Check if this is a function definition
|
||||||
|
if (/(?:function\s+\w+|=>\s*\{|\)\s*=>|\) =>|\):.*=>)\s*\{?/.test(line)) {
|
||||||
|
inLoggerFunc = false;
|
||||||
|
braceDepth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.includes("logger.") && !line.trim().startsWith("//") && !line.includes("const logger") && !line.includes("let logger") && !line.includes("logger:")) {
|
||||||
|
if (!foundLine) {
|
||||||
|
foundLine = i;
|
||||||
|
}
|
||||||
|
// logger used but not defined — go back and add definition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple approach: find the first { after export/signature and add logger
|
||||||
|
// This is heuristic and may need manual review
|
||||||
|
let firstBrace = false;
|
||||||
|
let fixed = false;
|
||||||
|
const newLines: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
|
||||||
|
if (!fixed && line.includes("logger.") && !line.trim().startsWith("//") && !line.includes("const logger") && !line.includes("let logger") && !line.includes("logger:") && !line.includes("import ")) {
|
||||||
|
// Go back to find the function body start and insert logger definition
|
||||||
|
let insertAt = i;
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
|
if (lines[j].includes("{") && /\b(?:function|=>|async|export|const\s+\w+\s*=|let\s+\w+\s*=)\b/.test(lines.slice(Math.max(0, j-5), j+1).join(" "))) {
|
||||||
|
insertAt = j + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Insert before the first use
|
||||||
|
// Adjust indentation
|
||||||
|
const indent = lines[insertAt].match(/^(\s*)/)?.[1] || "";
|
||||||
|
newLines.push(`${indent}const logger = createLogger(env);`);
|
||||||
|
console.log(` FIXED: ${filename}`);
|
||||||
|
fixed = true;
|
||||||
|
// Now continue adding remaining lines
|
||||||
|
for (let k = i; k < lines.length; k++) {
|
||||||
|
newLines.push(lines[k]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newLines.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixed ? newLines.join("\n") : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of FILES) {
|
||||||
|
let content = readFileSync(f, "utf-8");
|
||||||
|
const newContent = addLoggerLine(content, f);
|
||||||
|
if (newContent !== content) {
|
||||||
|
writeFileSync(f, newContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ const validateContent = process.argv.includes("--validate-content");
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("Validating database schema...");
|
console.log("Validating database schema...");
|
||||||
await validateDbSchema({ db });
|
await validateDbSchema({ db, env: process.env as unknown as Env });
|
||||||
console.log("✅ Database schema validated successfully\n");
|
console.log("✅ Database schema validated successfully\n");
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { loadLocalEnv } from "@server/utils/envUtils.js";
|
|
||||||
|
|
||||||
loadLocalEnv();
|
|
||||||
|
|
||||||
// Worktree-aware: `bun dw` writes per-worktree `.env.local` files to each
|
// Worktree-aware: `bun dw` writes per-worktree `.env.local` files to each
|
||||||
// workspace dir. PW_MODE=1 (set by `bun pw`) skips this so prod Infisical
|
// workspace dir. PW_MODE=1 (set by `bun pw`) skips this so prod Infisical
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
|
||||||
|
|
||||||
loadLocalEnv();
|
|
||||||
|
|
||||||
const requireEnv = ({ key }: { key: string }) => {
|
const requireEnv = ({ key }: { key: string }) => {
|
||||||
const value = process.env[key];
|
const value = process.env[key];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
|
||||||
|
|
||||||
loadLocalEnv();
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AppEnv,
|
AppEnv,
|
||||||
|
|||||||
@@ -5,9 +5,6 @@
|
|||||||
* or: ENV_FILE=.env infisical run --recursive --env=dev -- bun perf/load-test/setup.ts
|
* or: ENV_FILE=.env infisical run --recursive --env=dev -- bun perf/load-test/setup.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadLocalEnv } from "../../src/utils/envUtils.js";
|
|
||||||
loadLocalEnv();
|
|
||||||
|
|
||||||
import { writeFileSync } from "node:fs";
|
import { writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { ApiVersion, AppEnv, BillingInterval } from "@autumn/shared";
|
import { ApiVersion, AppEnv, BillingInterval } from "@autumn/shared";
|
||||||
|
|||||||
@@ -6,9 +6,6 @@
|
|||||||
* Run: cd server && bun perf/redis-bench/setup.ts
|
* Run: cd server && bun perf/redis-bench/setup.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadLocalEnv } from "../../src/utils/envUtils.js";
|
|
||||||
loadLocalEnv();
|
|
||||||
|
|
||||||
import { AppEnv, ApiVersion, type FullCustomer } from "@autumn/shared";
|
import { AppEnv, ApiVersion, type FullCustomer } from "@autumn/shared";
|
||||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements.js";
|
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements.js";
|
||||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts.js";
|
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts.js";
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { initInfisical } from "./external/infisical/initInfisical.js";
|
import { initInfisical } from "./external/infisical/initInfisical.js";
|
||||||
|
import { createLogger } from "./external/logtail/logtailUtils.js";
|
||||||
|
|
||||||
|
export const startCron = async (initialEnv: Env) => {
|
||||||
|
const env = await initInfisical(initialEnv);
|
||||||
|
const logger = createLogger(env);
|
||||||
|
|
||||||
await initInfisical();
|
|
||||||
const { warmupRegionalRedis } = await import("./external/redis/initRedis.js");
|
const { warmupRegionalRedis } = await import("./external/redis/initRedis.js");
|
||||||
await warmupRegionalRedis();
|
await warmupRegionalRedis();
|
||||||
|
|
||||||
@@ -8,7 +12,6 @@ await warmupRegionalRedis();
|
|||||||
// so resolveRedisV2 picks the right instance on each ctx build).
|
// so resolveRedisV2 picks the right instance on each ctx build).
|
||||||
await import("./internal/misc/redisV2Cache/redisV2CacheStore.js");
|
await import("./internal/misc/redisV2Cache/redisV2CacheStore.js");
|
||||||
await import("./internal/misc/cacheV2Ramp/cacheV2RampStore.js");
|
await import("./internal/misc/cacheV2Ramp/cacheV2RampStore.js");
|
||||||
const { logger } = await import("./external/logtail/logtailUtils.js");
|
|
||||||
const { startAllEdgeConfigPolling } = await import(
|
const { startAllEdgeConfigPolling } = await import(
|
||||||
"./internal/misc/edgeConfig/edgeConfigRegistry.js"
|
"./internal/misc/edgeConfig/edgeConfigRegistry.js"
|
||||||
);
|
);
|
||||||
@@ -19,10 +22,12 @@ await startAllEdgeConfigPolling({ logger });
|
|||||||
const { resolveAwsTaskIdentity } = await import(
|
const { resolveAwsTaskIdentity } = await import(
|
||||||
"./external/aws/ecs/awsTaskIdentity.js"
|
"./external/aws/ecs/awsTaskIdentity.js"
|
||||||
);
|
);
|
||||||
await resolveAwsTaskIdentity();
|
await resolveAwsTaskIdentity(env);
|
||||||
const { startBlueGreenSlotStorePolling } = await import(
|
const { startBlueGreenSlotStorePolling } = await import(
|
||||||
"./queue/blueGreen/blueGreenSlotStore.js"
|
"./queue/blueGreen/blueGreenSlotStore.js"
|
||||||
);
|
);
|
||||||
await startBlueGreenSlotStorePolling({ serviceName: "cron", logger });
|
await startBlueGreenSlotStorePolling({ serviceName: "cron", logger });
|
||||||
|
|
||||||
await import("./cron/cronInit.js");
|
const { startCronInit } = await import("./cron/cronInit.js");
|
||||||
|
await startCronInit(env);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import "../sentry.ts";
|
|
||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
import { initDrizzle } from "../db/initDrizzle.js";
|
import { initDrizzle } from "../db/initDrizzle.js";
|
||||||
import { startPgPoolMonitor, stopPgPoolMonitor } from "../db/pgPoolMonitor.js";
|
import { startPgPoolMonitor, stopPgPoolMonitor } from "../db/pgPoolMonitor.js";
|
||||||
import { logger } from "../external/logtail/logtailUtils.js";
|
import { createLogger } from "../external/logtail/logtailUtils.js";
|
||||||
|
import type { Logger } from "../external/logtail/logtailUtils.js";
|
||||||
import {
|
import {
|
||||||
describeSlotGate,
|
describeSlotGate,
|
||||||
isActiveSlot,
|
isActiveSlot,
|
||||||
@@ -20,8 +19,11 @@ import { runProductCron } from "./productCron/runProductCron.js";
|
|||||||
import { runResetCron } from "./resetCron/runResetCron.js";
|
import { runResetCron } from "./resetCron/runResetCron.js";
|
||||||
import type { CronContext } from "./utils/CronContext.js";
|
import type { CronContext } from "./utils/CronContext.js";
|
||||||
|
|
||||||
|
export const startCronInit = (env: Env) => {
|
||||||
|
const logger = createLogger(env);
|
||||||
|
|
||||||
const { db, client } = initDrizzle({ name: "cron", maxConnections: 40 });
|
const { db, client } = initDrizzle({ name: "cron", maxConnections: 40 });
|
||||||
startPgPoolMonitor();
|
startPgPoolMonitor(env);
|
||||||
startBlueGreenHeartbeat({ db, logger, serviceName: "cron" });
|
startBlueGreenHeartbeat({ db, logger, serviceName: "cron" });
|
||||||
|
|
||||||
const logCronHeartbeat = () => {
|
const logCronHeartbeat = () => {
|
||||||
@@ -38,7 +40,7 @@ const logCronHeartbeat = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
if (runtimeEnv.DISABLE_CRON === "true") {
|
if (env.DISABLE_CRON === "true") {
|
||||||
console.log(`Cron disabled!`);
|
console.log(`Cron disabled!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -70,11 +72,11 @@ const main = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
new CronJob(
|
new CronJob(
|
||||||
"* * * * *", // Run every minute
|
"* * * * *",
|
||||||
main,
|
main,
|
||||||
null, // onComplete
|
null,
|
||||||
true, // start immediately
|
true,
|
||||||
"UTC", // timezone (adjust as needed)
|
"UTC",
|
||||||
);
|
);
|
||||||
|
|
||||||
main();
|
main();
|
||||||
@@ -96,3 +98,4 @@ process.on("SIGINT", async () => {
|
|||||||
await client.end();
|
await client.end();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { getTableColumns, type SQL, sql } from "drizzle-orm";
|
import { getTableColumns, type SQL, sql } from "drizzle-orm";
|
||||||
import type { PgTable } from "drizzle-orm/pg-core";
|
import type { PgTable } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
@@ -116,7 +115,7 @@ export const isConnectionDropError = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Throws if the connection string looks like a production database. Single source of truth for this check. */
|
/** Throws if the connection string looks like a production database. Single source of truth for this check. */
|
||||||
export const assertNotProductionDb = (url = runtimeEnv.DATABASE_URL || "") => {
|
export const assertNotProductionDb = (env: Env, url = env.DATABASE_URL || "") => {
|
||||||
if (url.includes("us-east-2")) {
|
if (url.includes("us-east-2")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Refusing to run against production database (connection string contains us-east-2)",
|
"Refusing to run against production database (connection string contains us-east-2)",
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
|
|||||||
import type { SQLWrapper } from "drizzle-orm";
|
import type { SQLWrapper } from "drizzle-orm";
|
||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
import pg, { type PoolConfig } from "pg";
|
import pg, { type PoolConfig } from "pg";
|
||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
import { createLogger } from "../external/logtail/logtailUtils.js";
|
||||||
import { logger } from "../external/logtail/logtailUtils.js";
|
|
||||||
import { otelConfig } from "../utils/otel/otelConfig.js";
|
import { otelConfig } from "../utils/otel/otelConfig.js";
|
||||||
import { attachPoolErrorHandlers, registerPool } from "./pgPoolMonitor.js";
|
import { attachPoolErrorHandlers, registerPool } from "./pgPoolMonitor.js";
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ const normalizeDbExecute = <
|
|||||||
|
|
||||||
/** Creates a Drizzle pool with the given configuration. */
|
/** Creates a Drizzle pool with the given configuration. */
|
||||||
export const initDrizzle = ({
|
export const initDrizzle = ({
|
||||||
maxConnections = isProd ? 70 : 10,
|
maxConnections = 10,
|
||||||
replica = false,
|
replica = false,
|
||||||
connectTimeout = 5,
|
connectTimeout = 5,
|
||||||
databaseUrl,
|
databaseUrl,
|
||||||
@@ -53,11 +52,7 @@ export const initDrizzle = ({
|
|||||||
/** Pool name for monitor/error logs. Omit to skip registration. */
|
/** Pool name for monitor/error logs. Omit to skip registration. */
|
||||||
name?: string;
|
name?: string;
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const envDbUrl = replica
|
const dbUrl = databaseUrl || "";
|
||||||
? runtimeEnv.DATABASE_REPLICA_URL
|
|
||||||
: runtimeEnv.DATABASE_URL;
|
|
||||||
|
|
||||||
const dbUrl = databaseUrl || envDbUrl || "";
|
|
||||||
|
|
||||||
const client = new pg.Pool({
|
const client = new pg.Pool({
|
||||||
connectionString: dbUrl,
|
connectionString: dbUrl,
|
||||||
@@ -91,46 +86,68 @@ export const initDrizzle = ({
|
|||||||
return { db, client };
|
return { db, client };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Strict latency limits in prod; relaxed locally so dev pool warm-up doesn't kill tests.
|
export type DrizzleCli = ReturnType<typeof initDrizzle>["db"];
|
||||||
const isProd = runtimeEnv.NODE_ENV === "production";
|
|
||||||
|
|
||||||
const poolMaxFromEnv = ({
|
// ── Lazy-initialized pool singletons ──────────────────────────────────
|
||||||
envVar,
|
|
||||||
fallback,
|
let _initialized = false;
|
||||||
}: {
|
|
||||||
envVar:
|
export let client: ReturnType<typeof initDrizzle>["client"];
|
||||||
| "CRITICAL_DB_POOL_MAX"
|
export let db: DrizzleCli;
|
||||||
| "GENERAL_DB_POOL_MAX"
|
export let clientCritical: ReturnType<typeof initDrizzle>["client"];
|
||||||
| "REPLICA_DB_POOL_MAX";
|
export let dbCritical: DrizzleCli;
|
||||||
fallback: number;
|
export let clientGeneral: ReturnType<typeof initDrizzle>["client"];
|
||||||
}): number => {
|
export let dbGeneral: DrizzleCli;
|
||||||
const parsed = Number(runtimeEnv[envVar]);
|
export let clientReplica: ReturnType<typeof initDrizzle>["client"] | null = null;
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
export let dbReplica: DrizzleCli | null = null;
|
||||||
};
|
|
||||||
|
|
||||||
const PGBOUNCER_MAX_CLIENT_CONN = 7_600;
|
const PGBOUNCER_MAX_CLIENT_CONN = 7_600;
|
||||||
const BUDGETED_FLEET_PROCESSES = 150;
|
const BUDGETED_FLEET_PROCESSES = 150;
|
||||||
const BUDGETED_NON_SERVER_CONNECTIONS = 80;
|
const BUDGETED_NON_SERVER_CONNECTIONS = 80;
|
||||||
const POOL_BUDGET_HEADROOM = 0.85;
|
const POOL_BUDGET_HEADROOM = 0.85;
|
||||||
|
|
||||||
|
const poolMaxFromEnv = (
|
||||||
|
env: Env,
|
||||||
|
envVar: "CRITICAL_DB_POOL_MAX" | "GENERAL_DB_POOL_MAX" | "REPLICA_DB_POOL_MAX",
|
||||||
|
fallback: number,
|
||||||
|
): number => {
|
||||||
|
const parsed = Number(env[envVar]);
|
||||||
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all database pools with the given env bindings.
|
||||||
|
* Must be called once at startup before any other module accesses
|
||||||
|
* the `db`, `client`, etc. exports.
|
||||||
|
*/
|
||||||
|
export const initDrizzleModules = (env: Env) => {
|
||||||
|
if (_initialized) return;
|
||||||
|
_initialized = true;
|
||||||
|
|
||||||
|
const logger = createLogger(env);
|
||||||
|
const isProd = env.NODE_ENV === "production";
|
||||||
|
|
||||||
const PROD_POOL_MAX = {
|
const PROD_POOL_MAX = {
|
||||||
critical: 22,
|
critical: 22,
|
||||||
general: 14,
|
general: 14,
|
||||||
replica: 6,
|
replica: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
const criticalPoolMax = poolMaxFromEnv({
|
const criticalPoolMax = poolMaxFromEnv(
|
||||||
envVar: "CRITICAL_DB_POOL_MAX",
|
env,
|
||||||
fallback: isProd ? PROD_POOL_MAX.critical : 10,
|
"CRITICAL_DB_POOL_MAX",
|
||||||
});
|
isProd ? PROD_POOL_MAX.critical : 10,
|
||||||
const generalPoolMax = poolMaxFromEnv({
|
);
|
||||||
envVar: "GENERAL_DB_POOL_MAX",
|
const generalPoolMax = poolMaxFromEnv(
|
||||||
fallback: isProd ? PROD_POOL_MAX.general : 10,
|
env,
|
||||||
});
|
"GENERAL_DB_POOL_MAX",
|
||||||
const replicaPoolMax = poolMaxFromEnv({
|
isProd ? PROD_POOL_MAX.general : 10,
|
||||||
envVar: "REPLICA_DB_POOL_MAX",
|
);
|
||||||
fallback: PROD_POOL_MAX.replica,
|
const replicaPoolMax = poolMaxFromEnv(
|
||||||
});
|
env,
|
||||||
|
"REPLICA_DB_POOL_MAX",
|
||||||
|
PROD_POOL_MAX.replica,
|
||||||
|
);
|
||||||
|
|
||||||
const budgetedFleetConnections =
|
const budgetedFleetConnections =
|
||||||
BUDGETED_FLEET_PROCESSES *
|
BUDGETED_FLEET_PROCESSES *
|
||||||
@@ -146,42 +163,41 @@ if (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { db: dbCritical, client: clientCritical } = initDrizzle({
|
const criticalResult = initDrizzle({
|
||||||
name: "critical",
|
name: "critical",
|
||||||
maxConnections: criticalPoolMax,
|
maxConnections: criticalPoolMax,
|
||||||
connectTimeout: isProd ? 2 : 30,
|
connectTimeout: isProd ? 2 : 30,
|
||||||
databaseUrl: runtimeEnv.DATABASE_CRITICAL_URL,
|
databaseUrl: env.DATABASE_CRITICAL_URL,
|
||||||
poolConfig: {
|
poolConfig: {
|
||||||
application_name: "autumn-critical",
|
application_name: "autumn-critical",
|
||||||
query_timeout: isProd ? 2_000 : 30_000,
|
query_timeout: isProd ? 2_000 : 30_000,
|
||||||
// Keep warm conns to avoid TLS-handshake stampedes on bursty traffic.
|
|
||||||
min: Math.min(10, criticalPoolMax),
|
min: Math.min(10, criticalPoolMax),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
dbCritical = criticalResult.db;
|
||||||
|
clientCritical = criticalResult.client;
|
||||||
|
|
||||||
// -- General pool: used by all other endpoints --
|
const generalResult = initDrizzle({
|
||||||
export const { db: dbGeneral, client: clientGeneral } = initDrizzle({
|
|
||||||
name: "general",
|
name: "general",
|
||||||
maxConnections: generalPoolMax,
|
maxConnections: generalPoolMax,
|
||||||
connectTimeout: isProd ? 5 : 30,
|
connectTimeout: isProd ? 5 : 30,
|
||||||
|
databaseUrl: env.DATABASE_URL,
|
||||||
});
|
});
|
||||||
|
dbGeneral = generalResult.db;
|
||||||
|
clientGeneral = generalResult.client;
|
||||||
|
|
||||||
// -- Replica pool: used as fallback when primary is degraded --
|
if (env.DATABASE_REPLICA_URL) {
|
||||||
// Only created if DATABASE_REPLICA_URL is configured.
|
const replicaResult = initDrizzle({
|
||||||
const replicaResult = runtimeEnv.DATABASE_REPLICA_URL
|
|
||||||
? initDrizzle({
|
|
||||||
name: "replica",
|
name: "replica",
|
||||||
replica: true,
|
replica: true,
|
||||||
maxConnections: replicaPoolMax,
|
maxConnections: replicaPoolMax,
|
||||||
|
databaseUrl: env.DATABASE_REPLICA_URL,
|
||||||
connectTimeout: null,
|
connectTimeout: null,
|
||||||
})
|
});
|
||||||
: null;
|
dbReplica = replicaResult.db;
|
||||||
export const dbReplica = replicaResult?.db ?? null;
|
clientReplica = replicaResult.client;
|
||||||
export const clientReplica = replicaResult?.client ?? null;
|
}
|
||||||
|
|
||||||
// Backward-compatible exports — existing code that imports `db` or `client`
|
client = clientGeneral;
|
||||||
// gets the general pool automatically.
|
db = dbGeneral;
|
||||||
export const client = clientGeneral;
|
};
|
||||||
export const db = dbGeneral;
|
|
||||||
|
|
||||||
export type DrizzleCli = ReturnType<typeof initDrizzle>["db"];
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import type { SQL } from "drizzle-orm";
|
import type { SQL } from "drizzle-orm";
|
||||||
import type { Pool } from "pg";
|
import type { Pool } from "pg";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
|
import type { Logger } from "@/external/logtail/logtailUtils.js";
|
||||||
import { isConnectionDropError } from "./dbUtils.js";
|
import { isConnectionDropError } from "./dbUtils.js";
|
||||||
import { type DrizzleCli, dbCritical, dbReplica } from "./initDrizzle.js";
|
import { type DrizzleCli, dbCritical, dbReplica } from "./initDrizzle.js";
|
||||||
|
|
||||||
@@ -38,6 +38,17 @@ let probeClient: Pool | null = null;
|
|||||||
let failureCount = 0;
|
let failureCount = 0;
|
||||||
let windowStartedAt = Date.now();
|
let windowStartedAt = Date.now();
|
||||||
|
|
||||||
|
// Lazily initialized from initPgHealthMonitor
|
||||||
|
let _logger: Logger | null = null;
|
||||||
|
let _hasReplica = false;
|
||||||
|
|
||||||
|
const getLogger = (): Logger => {
|
||||||
|
if (!_logger) {
|
||||||
|
throw new Error("PgHealthMonitor not initialized — call initPgHealthMonitor first");
|
||||||
|
}
|
||||||
|
return _logger;
|
||||||
|
};
|
||||||
|
|
||||||
const resetFailureWindow = (now = Date.now()) => {
|
const resetFailureWindow = (now = Date.now()) => {
|
||||||
failureCount = 0;
|
failureCount = 0;
|
||||||
windowStartedAt = now;
|
windowStartedAt = now;
|
||||||
@@ -46,49 +57,12 @@ const resetFailureWindow = (now = Date.now()) => {
|
|||||||
/** Get the current DB health state. */
|
/** Get the current DB health state. */
|
||||||
export const getDbHealth = (): PgHealth => state;
|
export const getDbHealth = (): PgHealth => state;
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the health monitor with a pg pool for probing.
|
|
||||||
* Call once at startup. The probe client should be the critical pool's raw client.
|
|
||||||
*/
|
|
||||||
export const initPgHealthMonitor = ({ client }: { client: Pool }): void => {
|
|
||||||
probeClient = client;
|
|
||||||
logger.info("[PgHealthMonitor] Initialized", { type: "pg_health_init" });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record a successful query against the primary DB.
|
|
||||||
* No-op: recovery is driven by the probe, not by application queries.
|
|
||||||
*/
|
|
||||||
export const recordDbSuccess = (): void => {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record a slow or failed query against the primary DB.
|
|
||||||
* Called when a critical-pool query exceeds the latency threshold or throws.
|
|
||||||
*/
|
|
||||||
export const recordDbFailure = (): void => {
|
|
||||||
if (state === PgHealth.Degraded) return;
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Reset counter if the window has elapsed
|
|
||||||
if (now - windowStartedAt > FAILURE_WINDOW_MS) {
|
|
||||||
failureCount = 0;
|
|
||||||
windowStartedAt = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
failureCount++;
|
|
||||||
|
|
||||||
if (failureCount >= FAILURE_THRESHOLD) {
|
|
||||||
switchToDegraded();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const switchToDegraded = (): void => {
|
const switchToDegraded = (): void => {
|
||||||
if (state === PgHealth.Degraded) return;
|
if (state === PgHealth.Degraded) return;
|
||||||
|
|
||||||
state = PgHealth.Degraded;
|
state = PgHealth.Degraded;
|
||||||
|
|
||||||
logger.error("[PgHealthMonitor] ENTERING DEGRADED MODE", {
|
getLogger().error("[PgHealthMonitor] ENTERING DEGRADED MODE", {
|
||||||
type: "pg_health_degraded",
|
type: "pg_health_degraded",
|
||||||
failureCount,
|
failureCount,
|
||||||
windowMs: FAILURE_WINDOW_MS,
|
windowMs: FAILURE_WINDOW_MS,
|
||||||
@@ -107,7 +81,7 @@ const switchToHealthy = (): void => {
|
|||||||
resetFailureWindow();
|
resetFailureWindow();
|
||||||
firstProbeSuccessAt = null;
|
firstProbeSuccessAt = null;
|
||||||
|
|
||||||
logger.info("[PgHealthMonitor] RECOVERED to HEALTHY", {
|
getLogger().info("[PgHealthMonitor] RECOVERED to HEALTHY", {
|
||||||
type: "pg_health_recovered",
|
type: "pg_health_recovered",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,7 +111,7 @@ const startProbe = (): void => {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (!firstProbeSuccessAt) {
|
if (!firstProbeSuccessAt) {
|
||||||
firstProbeSuccessAt = now;
|
firstProbeSuccessAt = now;
|
||||||
logger.info(
|
getLogger().info(
|
||||||
"[PgHealthMonitor] Probe succeeded, waiting for stability",
|
"[PgHealthMonitor] Probe succeeded, waiting for stability",
|
||||||
{
|
{
|
||||||
type: "pg_health_probe",
|
type: "pg_health_probe",
|
||||||
@@ -151,7 +125,7 @@ const startProbe = (): void => {
|
|||||||
} catch {
|
} catch {
|
||||||
// Probe failed — reset stability timer
|
// Probe failed — reset stability timer
|
||||||
if (firstProbeSuccessAt) {
|
if (firstProbeSuccessAt) {
|
||||||
logger.warn(
|
getLogger().warn(
|
||||||
"[PgHealthMonitor] Probe failed, resetting stability timer",
|
"[PgHealthMonitor] Probe failed, resetting stability timer",
|
||||||
{
|
{
|
||||||
type: "pg_health_probe",
|
type: "pg_health_probe",
|
||||||
@@ -172,6 +146,51 @@ const stopProbe = (): void => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the health monitor with a pg pool for probing and env for logging.
|
||||||
|
* Call once at startup.
|
||||||
|
*/
|
||||||
|
export const initPgHealthMonitor = ({
|
||||||
|
client,
|
||||||
|
env,
|
||||||
|
}: {
|
||||||
|
client: Pool;
|
||||||
|
env: Env;
|
||||||
|
}): void => {
|
||||||
|
probeClient = client;
|
||||||
|
_logger = createLogger(env);
|
||||||
|
_hasReplica = !!env.DATABASE_REPLICA_URL;
|
||||||
|
getLogger().info("[PgHealthMonitor] Initialized", { type: "pg_health_init" });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a successful query against the primary DB.
|
||||||
|
* No-op: recovery is driven by the probe, not by application queries.
|
||||||
|
*/
|
||||||
|
export const recordDbSuccess = (): void => {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a slow or failed query against the primary DB.
|
||||||
|
* Called when a critical-pool query exceeds the latency threshold or throws.
|
||||||
|
*/
|
||||||
|
export const recordDbFailure = (): void => {
|
||||||
|
if (state === PgHealth.Degraded) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Reset counter if the window has elapsed
|
||||||
|
if (now - windowStartedAt > FAILURE_WINDOW_MS) {
|
||||||
|
failureCount = 0;
|
||||||
|
windowStartedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
failureCount++;
|
||||||
|
|
||||||
|
if (failureCount >= FAILURE_THRESHOLD) {
|
||||||
|
switchToDegraded();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a query with automatic health tracking and replica fallback.
|
* Execute a query with automatic health tracking and replica fallback.
|
||||||
* - If DEGRADED and a replica exists, queries the replica instead.
|
* - If DEGRADED and a replica exists, queries the replica instead.
|
||||||
@@ -244,14 +263,14 @@ export const getPgHealthState = (): {
|
|||||||
failureCount,
|
failureCount,
|
||||||
probeActive: probeInterval !== null,
|
probeActive: probeInterval !== null,
|
||||||
firstProbeSuccessAt,
|
firstProbeSuccessAt,
|
||||||
hasReplica: !!runtimeEnv.DATABASE_REPLICA_URL,
|
hasReplica: _hasReplica,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Force DEGRADED state (for testing). Does NOT start the recovery probe. */
|
/** Force DEGRADED state (for testing). Does NOT start the recovery probe. */
|
||||||
export const forceDegraded = (): void => {
|
export const forceDegraded = (): void => {
|
||||||
state = PgHealth.Degraded;
|
state = PgHealth.Degraded;
|
||||||
resetFailureWindow();
|
resetFailureWindow();
|
||||||
logger.info("[PgHealthMonitor] FORCE DEGRADED (test)", {
|
getLogger().info("[PgHealthMonitor] FORCE DEGRADED (test)", {
|
||||||
type: "pg_health_force",
|
type: "pg_health_force",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -262,7 +281,7 @@ export const forceHealthy = (): void => {
|
|||||||
resetFailureWindow();
|
resetFailureWindow();
|
||||||
firstProbeSuccessAt = null;
|
firstProbeSuccessAt = null;
|
||||||
stopProbe();
|
stopProbe();
|
||||||
logger.info("[PgHealthMonitor] FORCE HEALTHY (test)", {
|
getLogger().info("[PgHealthMonitor] FORCE HEALTHY (test)", {
|
||||||
type: "pg_health_force",
|
type: "pg_health_force",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import type { Pool } from "pg";
|
import type { Pool } from "pg";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
|
import type { Logger } from "@/external/logtail/logtailUtils.js";
|
||||||
|
|
||||||
type RegisteredPool = {
|
type RegisteredPool = {
|
||||||
pool: Pool;
|
pool: Pool;
|
||||||
@@ -11,12 +11,18 @@ type RegisteredPool = {
|
|||||||
const registry = new Map<string, RegisteredPool>();
|
const registry = new Map<string, RegisteredPool>();
|
||||||
let snapshotInterval: ReturnType<typeof setInterval> | null = null;
|
let snapshotInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
const getRole = (): string => {
|
let _logger: Logger | null = null;
|
||||||
if (runtimeEnv.WORKER === "true") return "worker";
|
let _role = "http";
|
||||||
if (runtimeEnv.CRON === "true") return "cron";
|
|
||||||
return "http";
|
const getLogger = (): Logger => {
|
||||||
|
if (!_logger) {
|
||||||
|
throw new Error("PgPoolMonitor not initialized — call startPgPoolMonitor first");
|
||||||
|
}
|
||||||
|
return _logger;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRole = (): string => _role;
|
||||||
|
|
||||||
export const registerPool = ({
|
export const registerPool = ({
|
||||||
pool,
|
pool,
|
||||||
name,
|
name,
|
||||||
@@ -37,7 +43,7 @@ export const attachPoolErrorHandlers = ({
|
|||||||
name: string;
|
name: string;
|
||||||
}): void => {
|
}): void => {
|
||||||
pool.on("error", (err: Error & { code?: string }) => {
|
pool.on("error", (err: Error & { code?: string }) => {
|
||||||
logger.warn("pg_pool_error", {
|
getLogger().warn("pg_pool_error", {
|
||||||
type: "pg_pool_error",
|
type: "pg_pool_error",
|
||||||
pool: name,
|
pool: name,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
@@ -49,31 +55,20 @@ export const attachPoolErrorHandlers = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const emitSnapshot = (): void => {
|
const emitSnapshot = (): void => {};
|
||||||
// const role = getRole();
|
|
||||||
// for (const { pool, name, max } of registry.values()) {
|
|
||||||
// const totalCount = pool.totalCount;
|
|
||||||
// const idleCount = pool.idleCount;
|
|
||||||
// const waitingCount = pool.waitingCount;
|
|
||||||
// logger.debug("pg_pool_stats", {
|
|
||||||
// type: "pg_pool_stats",
|
|
||||||
// pool: name,
|
|
||||||
// pid: process.pid,
|
|
||||||
// role,
|
|
||||||
// totalCount,
|
|
||||||
// idleCount,
|
|
||||||
// waitingCount,
|
|
||||||
// max,
|
|
||||||
// utilization: max > 0 ? totalCount / max : 0,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
export const startPgPoolMonitor = (intervalMs = 30_000): void => {
|
export const startPgPoolMonitor = (env: Env, intervalMs = 30_000): void => {
|
||||||
if (snapshotInterval) return;
|
if (snapshotInterval) return;
|
||||||
|
|
||||||
|
_logger = createLogger(env);
|
||||||
|
|
||||||
|
if (env.WORKER === "true") _role = "worker";
|
||||||
|
else if (env.CRON === "true") _role = "cron";
|
||||||
|
else _role = "http";
|
||||||
|
|
||||||
snapshotInterval = setInterval(emitSnapshot, intervalMs);
|
snapshotInterval = setInterval(emitSnapshot, intervalMs);
|
||||||
if (snapshotInterval.unref) snapshotInterval.unref();
|
if (snapshotInterval.unref) snapshotInterval.unref();
|
||||||
logger.info("[PgPoolMonitor] Started", {
|
getLogger().info("[PgPoolMonitor] Started", {
|
||||||
type: "pg_pool_monitor_start",
|
type: "pg_pool_monitor_start",
|
||||||
intervalMs,
|
intervalMs,
|
||||||
pools: Array.from(registry.keys()),
|
pools: Array.from(registry.keys()),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
|
|
||||||
const hash = (value: string) =>
|
const hash = (value: string) =>
|
||||||
@@ -33,10 +32,10 @@ export const redactDatabaseUrl = (databaseUrl?: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRedactedDatabaseUrls = () => ({
|
export const getRedactedDatabaseUrls = (env: Env) => ({
|
||||||
primary: redactDatabaseUrl(runtimeEnv.DATABASE_URL),
|
primary: redactDatabaseUrl(env.DATABASE_URL),
|
||||||
replica: redactDatabaseUrl(runtimeEnv.DATABASE_REPLICA_URL),
|
replica: redactDatabaseUrl(env.DATABASE_REPLICA_URL),
|
||||||
critical: redactDatabaseUrl(
|
critical: redactDatabaseUrl(
|
||||||
runtimeEnv.DATABASE_CRITICAL_URL || runtimeEnv.DATABASE_URL,
|
env.DATABASE_CRITICAL_URL || env.DATABASE_URL,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import * as schema from "@autumn/shared";
|
import * as schema from "@autumn/shared";
|
||||||
import { is } from "drizzle-orm";
|
import { is } from "drizzle-orm";
|
||||||
import { PgTable } from "drizzle-orm/pg-core";
|
import { PgTable } from "drizzle-orm/pg-core";
|
||||||
import { logger } from "../external/logtail/logtailUtils";
|
import { createLogger } from "../external/logtail/logtailUtils";
|
||||||
import type { DrizzleCli } from "./initDrizzle";
|
import type { DrizzleCli } from "./initDrizzle";
|
||||||
|
|
||||||
const SKIP_TABLES = ["migrationErrors"];
|
const SKIP_TABLES = ["migrationErrors"];
|
||||||
|
|
||||||
export const validateDbSchema = async ({ db }: { db: DrizzleCli }) => {
|
export const validateDbSchema = async ({ db, env }: { db: DrizzleCli; env: Env }) => {
|
||||||
|
const logger = createLogger(env);
|
||||||
// Dynamically get all tables from schema (exclude relations)
|
// Dynamically get all tables from schema (exclude relations)
|
||||||
|
|
||||||
const tableEntries = Object.entries(schema)
|
const tableEntries = Object.entries(schema)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { readdirSync, readFileSync } from "node:fs";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { logger } from "../external/logtail/logtailUtils";
|
import { createLogger } from "../external/logtail/logtailUtils";
|
||||||
import type { DrizzleCli } from "./initDrizzle";
|
import type { DrizzleCli } from "./initDrizzle";
|
||||||
|
|
||||||
type SqlFunction = {
|
type SqlFunction = {
|
||||||
@@ -64,10 +64,13 @@ const discoverSqlFunctions = (): SqlFunction[] => {
|
|||||||
export const validateSqlFunctions = async ({
|
export const validateSqlFunctions = async ({
|
||||||
db,
|
db,
|
||||||
validateContent = false,
|
validateContent = false,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
validateContent?: boolean;
|
validateContent?: boolean;
|
||||||
|
env: Env;
|
||||||
}) => {
|
}) => {
|
||||||
|
const logger = createLogger(env);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
// Dynamically discover SQL functions from source files
|
// Dynamically discover SQL functions from source files
|
||||||
|
|||||||
6
server/src/external/ai/initAi.ts
vendored
6
server/src/external/ai/initAi.ts
vendored
@@ -1,8 +1,8 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { createAnthropic } from "@ai-sdk/anthropic";
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
||||||
|
|
||||||
export const anthropicClient = runtimeEnv.ANTHROPIC_API_KEY
|
export const initAi = (env: Env) =>
|
||||||
|
env.ANTHROPIC_API_KEY
|
||||||
? createAnthropic({
|
? createAnthropic({
|
||||||
apiKey: runtimeEnv.ANTHROPIC_API_KEY,
|
apiKey: env.ANTHROPIC_API_KEY,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
19
server/src/external/autumn/autumnCli.ts
vendored
19
server/src/external/autumn/autumnCli.ts
vendored
@@ -1,5 +1,4 @@
|
|||||||
/** biome-ignore-all lint/suspicious/noExplicitAny: AutumnInt is used for internal testing & scripts */
|
/** biome-ignore-all lint/suspicious/noExplicitAny: AutumnInt is used for internal testing & scripts */
|
||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import {
|
import {
|
||||||
type ApiBaseEntity,
|
type ApiBaseEntity,
|
||||||
type ApiCusFeatureV3,
|
type ApiCusFeatureV3,
|
||||||
@@ -72,6 +71,7 @@ export class AutumnInt {
|
|||||||
private apiKey: string;
|
private apiKey: string;
|
||||||
public headers: Record<string, string>;
|
public headers: Record<string, string>;
|
||||||
public baseUrl: string;
|
public baseUrl: string;
|
||||||
|
private _env?: Env;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -81,6 +81,7 @@ export class AutumnInt {
|
|||||||
orgConfig,
|
orgConfig,
|
||||||
liveUrl = false,
|
liveUrl = false,
|
||||||
skipCacheDeletion = false,
|
skipCacheDeletion = false,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
@@ -89,10 +90,12 @@ export class AutumnInt {
|
|||||||
orgConfig?: Partial<OrgConfig>;
|
orgConfig?: Partial<OrgConfig>;
|
||||||
liveUrl?: boolean;
|
liveUrl?: boolean;
|
||||||
skipCacheDeletion?: boolean;
|
skipCacheDeletion?: boolean;
|
||||||
|
env?: Env;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
// this.apiKey = apiKey || runtimeEnv.AUTUMN_API_KEY || "";
|
this._env = env;
|
||||||
|
// this.apiKey = apiKey || env.AUTUMN_API_KEY || "";
|
||||||
this.apiKey =
|
this.apiKey =
|
||||||
apiKey || secretKey || runtimeEnv.UNIT_TEST_AUTUMN_SECRET_KEY || "";
|
apiKey || secretKey || env?.UNIT_TEST_AUTUMN_SECRET_KEY || "";
|
||||||
|
|
||||||
this.headers = {
|
this.headers = {
|
||||||
Authorization: `Bearer ${this.apiKey}`,
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
@@ -107,7 +110,7 @@ export class AutumnInt {
|
|||||||
this.headers["org-config"] = JSON.stringify(orgConfig);
|
this.headers["org-config"] = JSON.stringify(orgConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envBase = runtimeEnv.AUTUMN_TEST_BASE_URL;
|
const envBase = env?.AUTUMN_TEST_BASE_URL;
|
||||||
const envBaseUrl = envBase ? `${envBase.replace(/\/$/, "")}/v1` : null;
|
const envBaseUrl = envBase ? `${envBase.replace(/\/$/, "")}/v1` : null;
|
||||||
this.baseUrl =
|
this.baseUrl =
|
||||||
baseUrl ||
|
baseUrl ||
|
||||||
@@ -313,7 +316,7 @@ export class AutumnInt {
|
|||||||
Object.keys(headers).length > 0 ? headers : undefined,
|
Object.keys(headers).length > 0 ? headers : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
|
const concurrency = Number(this._env?.TEST_FILE_CONCURRENCY || "0");
|
||||||
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
|
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
|
||||||
const finalTimeout = timeout ?? defaultTimeout;
|
const finalTimeout = timeout ?? defaultTimeout;
|
||||||
if (finalTimeout) {
|
if (finalTimeout) {
|
||||||
@@ -1216,7 +1219,7 @@ export class AutumnInt {
|
|||||||
Object.keys(headers).length > 0 ? headers : undefined,
|
Object.keys(headers).length > 0 ? headers : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
|
const concurrency = Number(this._env?.TEST_FILE_CONCURRENCY || "0");
|
||||||
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
|
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
|
||||||
const finalTimeout = timeout ?? defaultTimeout;
|
const finalTimeout = timeout ?? defaultTimeout;
|
||||||
if (finalTimeout) {
|
if (finalTimeout) {
|
||||||
@@ -1254,7 +1257,7 @@ export class AutumnInt {
|
|||||||
): Promise<TResponse> => {
|
): Promise<TResponse> => {
|
||||||
const data = await this.post(`/billing.create_schedule`, params);
|
const data = await this.post(`/billing.create_schedule`, params);
|
||||||
|
|
||||||
const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
|
const concurrency = Number(this._env?.TEST_FILE_CONCURRENCY || "0");
|
||||||
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
|
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
|
||||||
const finalTimeout = timeout ?? defaultTimeout;
|
const finalTimeout = timeout ?? defaultTimeout;
|
||||||
if (finalTimeout) {
|
if (finalTimeout) {
|
||||||
@@ -1285,7 +1288,7 @@ export class AutumnInt {
|
|||||||
Object.keys(headers).length > 0 ? headers : undefined,
|
Object.keys(headers).length > 0 ? headers : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
|
const concurrency = Number(this._env?.TEST_FILE_CONCURRENCY || "0");
|
||||||
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
|
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
|
||||||
const finalTimeout = timeout ?? defaultTimeout;
|
const finalTimeout = timeout ?? defaultTimeout;
|
||||||
if (finalTimeout) {
|
if (finalTimeout) {
|
||||||
|
|||||||
7
server/src/external/autumn/autumnRpcCli.ts
vendored
7
server/src/external/autumn/autumnRpcCli.ts
vendored
@@ -1,5 +1,4 @@
|
|||||||
/** biome-ignore-all lint/suspicious/noExplicitAny: RPC test client needs flexible payload typing */
|
/** biome-ignore-all lint/suspicious/noExplicitAny: RPC test client needs flexible payload typing */
|
||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { ErrCode, type OrgConfig } from "@autumn/shared";
|
import { ErrCode, type OrgConfig } from "@autumn/shared";
|
||||||
import AutumnError from "./autumnCli.js";
|
import AutumnError from "./autumnCli.js";
|
||||||
|
|
||||||
@@ -15,6 +14,7 @@ export class AutumnRpcCli {
|
|||||||
version,
|
version,
|
||||||
orgConfig,
|
orgConfig,
|
||||||
liveUrl = false,
|
liveUrl = false,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
@@ -22,9 +22,10 @@ export class AutumnRpcCli {
|
|||||||
version?: string;
|
version?: string;
|
||||||
orgConfig?: Partial<OrgConfig>;
|
orgConfig?: Partial<OrgConfig>;
|
||||||
liveUrl?: boolean;
|
liveUrl?: boolean;
|
||||||
|
env?: Env;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
this.apiKey =
|
this.apiKey =
|
||||||
apiKey || secretKey || runtimeEnv.UNIT_TEST_AUTUMN_SECRET_KEY || "";
|
apiKey || secretKey || env?.UNIT_TEST_AUTUMN_SECRET_KEY || "";
|
||||||
|
|
||||||
this.headers = {
|
this.headers = {
|
||||||
Authorization: `Bearer ${this.apiKey}`,
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
@@ -39,7 +40,7 @@ export class AutumnRpcCli {
|
|||||||
this.headers["org-config"] = JSON.stringify(orgConfig);
|
this.headers["org-config"] = JSON.stringify(orgConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envBase = runtimeEnv.AUTUMN_TEST_BASE_URL;
|
const envBase = env?.AUTUMN_TEST_BASE_URL;
|
||||||
const envBaseUrl = envBase ? `${envBase.replace(/\/$/, "")}/v1` : null;
|
const envBaseUrl = envBase ? `${envBase.replace(/\/$/, "")}/v1` : null;
|
||||||
this.baseUrl =
|
this.baseUrl =
|
||||||
baseUrl ||
|
baseUrl ||
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { ErrCode } from "@autumn/shared";
|
import { ErrCode } from "@autumn/shared";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Webhook } from "svix";
|
import { Webhook } from "svix";
|
||||||
@@ -10,6 +9,7 @@ export const autumnWebhookRouter = new Hono<HonoEnv>();
|
|||||||
const verifyAutumnWebhook = async ({
|
const verifyAutumnWebhook = async ({
|
||||||
rawBody,
|
rawBody,
|
||||||
headers,
|
headers,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
rawBody: string;
|
rawBody: string;
|
||||||
headers: {
|
headers: {
|
||||||
@@ -17,8 +17,9 @@ const verifyAutumnWebhook = async ({
|
|||||||
svixTimestamp: string | undefined;
|
svixTimestamp: string | undefined;
|
||||||
svixSignature: string | undefined;
|
svixSignature: string | undefined;
|
||||||
};
|
};
|
||||||
|
env: Env;
|
||||||
}) => {
|
}) => {
|
||||||
const wh = new Webhook(runtimeEnv.AUTUMN_WEBHOOK_SECRET!);
|
const wh = new Webhook(env.AUTUMN_WEBHOOK_SECRET!);
|
||||||
|
|
||||||
const { svixId, svixTimestamp, svixSignature } = headers;
|
const { svixId, svixTimestamp, svixSignature } = headers;
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ autumnWebhookRouter.post("", async (c) => {
|
|||||||
svixTimestamp: c.req.header("svix-timestamp"),
|
svixTimestamp: c.req.header("svix-timestamp"),
|
||||||
svixSignature: c.req.header("svix-signature"),
|
svixSignature: c.req.header("svix-signature"),
|
||||||
},
|
},
|
||||||
|
env: c.env,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Received webhook from autumn");
|
console.log("Received webhook from autumn");
|
||||||
|
|||||||
15
server/src/external/aws/ecs/awsTaskIdentity.ts
vendored
15
server/src/external/aws/ecs/awsTaskIdentity.ts
vendored
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,15 +49,17 @@ const constructServiceArn = ({
|
|||||||
* Reads the ECS task metadata endpoint to resolve this task's identity.
|
* Reads the ECS task metadata endpoint to resolve this task's identity.
|
||||||
* Cached for process lifetime — neither field changes for a running task.
|
* Cached for process lifetime — neither field changes for a running task.
|
||||||
*/
|
*/
|
||||||
export const resolveAwsTaskIdentity = async (): Promise<AwsTaskIdentity> => {
|
export const resolveAwsTaskIdentity = async (
|
||||||
|
env: Env,
|
||||||
|
): Promise<AwsTaskIdentity> => {
|
||||||
if (identityResolved && cachedIdentity) return cachedIdentity;
|
if (identityResolved && cachedIdentity) return cachedIdentity;
|
||||||
if (identityPromise) return identityPromise;
|
if (identityPromise) return identityPromise;
|
||||||
|
|
||||||
identityPromise = (async (): Promise<AwsTaskIdentity> => {
|
identityPromise = (async (): Promise<AwsTaskIdentity> => {
|
||||||
const imageSha =
|
const imageSha =
|
||||||
runtimeEnv.FC_GIT_COMMIT_SHA || runtimeEnv.IMAGE_TAG || null;
|
env.FC_GIT_COMMIT_SHA || env.IMAGE_TAG || null;
|
||||||
|
|
||||||
const metadataUri = runtimeEnv.ECS_CONTAINER_METADATA_URI_V4;
|
const metadataUri = env.ECS_CONTAINER_METADATA_URI_V4;
|
||||||
let serviceArn: string | null = null;
|
let serviceArn: string | null = null;
|
||||||
|
|
||||||
if (metadataUri) {
|
if (metadataUri) {
|
||||||
@@ -95,7 +96,7 @@ export const resolveAwsTaskIdentity = async (): Promise<AwsTaskIdentity> => {
|
|||||||
`[awsTaskIdentity] ECS metadata fetch failed: ${error instanceof Error ? error.message : error}; gate will fail open`,
|
`[awsTaskIdentity] ECS metadata fetch failed: ${error instanceof Error ? error.message : error}; gate will fail open`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (runtimeEnv.NODE_ENV === "production") {
|
} else if (env.NODE_ENV === "production") {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[awsTaskIdentity] ECS_CONTAINER_METADATA_URI_V4 unset in production — gate will fail open",
|
"[awsTaskIdentity] ECS_CONTAINER_METADATA_URI_V4 unset in production — gate will fail open",
|
||||||
);
|
);
|
||||||
@@ -119,7 +120,3 @@ export const getAwsTaskIdentity = (): AwsTaskIdentity | null => cachedIdentity;
|
|||||||
*/
|
*/
|
||||||
export const hasAwsTaskIdentity = (): boolean =>
|
export const hasAwsTaskIdentity = (): boolean =>
|
||||||
Boolean(cachedIdentity?.serviceArn);
|
Boolean(cachedIdentity?.serviceArn);
|
||||||
|
|
||||||
// Fire-and-forget at module load so server, workers, and cron all get
|
|
||||||
// identity resolved without each entry point having to await explicitly.
|
|
||||||
void resolveAwsTaskIdentity();
|
|
||||||
|
|||||||
5
server/src/external/aws/ecs/onAwsEcs.ts
vendored
5
server/src/external/aws/ecs/onAwsEcs.ts
vendored
@@ -5,6 +5,5 @@
|
|||||||
* so it's the canonical "am I on AWS?" gate — same one
|
* so it's the canonical "am I on AWS?" gate — same one
|
||||||
* `awsTaskIdentity` uses to discover the running service.
|
* `awsTaskIdentity` uses to discover the running service.
|
||||||
*/
|
*/
|
||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
export const onAwsEcs = (env: Env): boolean =>
|
||||||
export const onAwsEcs = (): boolean =>
|
Boolean(env.ECS_CONTAINER_METADATA_URI_V4);
|
||||||
Boolean(runtimeEnv.ECS_CONTAINER_METADATA_URI_V4);
|
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import {
|
import {
|
||||||
CreateScheduleCommand,
|
CreateScheduleCommand,
|
||||||
DeleteScheduleCommand,
|
DeleteScheduleCommand,
|
||||||
ResourceNotFoundException,
|
ResourceNotFoundException,
|
||||||
} from "@aws-sdk/client-scheduler";
|
} from "@aws-sdk/client-scheduler";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import { extractLocalEndpoint } from "@/queue/initSqs.js";
|
import { extractLocalEndpoint } from "@/queue/initSqs.js";
|
||||||
import { schedulerClient } from "./initEventBridge.js";
|
import { schedulerClient } from "./initEventBridge.js";
|
||||||
|
|
||||||
const isLocalQueue = (): boolean =>
|
const isLocalQueue = (env: Env): boolean =>
|
||||||
!!extractLocalEndpoint({ queueUrl: runtimeEnv.SQS_QUEUE_URL_V2 });
|
!!extractLocalEndpoint({ queueUrl: env.SQS_QUEUE_URL_V2 });
|
||||||
|
|
||||||
const SCHEDULE_GROUP = "default";
|
const SCHEDULE_GROUP = "default";
|
||||||
const SCHEDULER_ROLE_ARN = runtimeEnv.AWS_EVENTBRIDGE_SCHEDULER_ROLE_ARN || "";
|
|
||||||
|
export const getSchedulerRoleArn = (env: Env) =>
|
||||||
|
env.AWS_EVENTBRIDGE_SCHEDULER_ROLE_ARN || "";
|
||||||
|
|
||||||
/** Derives SQS ARN from URL: https://sqs.<region>.amazonaws.com/<account>/<name> -> arn:aws:sqs:<region>:<account>:<name> */
|
/** Derives SQS ARN from URL: https://sqs.<region>.amazonaws.com/<account>/<name> -> arn:aws:sqs:<region>:<account>:<name> */
|
||||||
const getSqsQueueArn = (): string => {
|
const getSqsQueueArn = (env: Env): string => {
|
||||||
const url = runtimeEnv.SQS_QUEUE_URL_V2 || "";
|
const url = env.SQS_QUEUE_URL_V2 || "";
|
||||||
const match = url.match(
|
const match = url.match(
|
||||||
/^https:\/\/sqs\.([a-z0-9-]+)\.amazonaws\.com\/(\d+)\/(.+)$/,
|
/^https:\/\/sqs\.([a-z0-9-]+)\.amazonaws\.com\/(\d+)\/(.+)$/,
|
||||||
);
|
);
|
||||||
@@ -28,17 +29,21 @@ const getSqsQueueArn = (): string => {
|
|||||||
|
|
||||||
/** Creates a one-shot EventBridge schedule that delivers an SQS message at scheduleAt */
|
/** Creates a one-shot EventBridge schedule that delivers an SQS message at scheduleAt */
|
||||||
export const createSchedule = async ({
|
export const createSchedule = async ({
|
||||||
|
env,
|
||||||
scheduleName,
|
scheduleName,
|
||||||
scheduleAt,
|
scheduleAt,
|
||||||
sqsMessageBody,
|
sqsMessageBody,
|
||||||
messageGroupId,
|
messageGroupId,
|
||||||
}: {
|
}: {
|
||||||
|
env: Env;
|
||||||
scheduleName: string;
|
scheduleName: string;
|
||||||
scheduleAt: Date;
|
scheduleAt: Date;
|
||||||
sqsMessageBody: string;
|
sqsMessageBody: string;
|
||||||
messageGroupId: string;
|
messageGroupId: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (isLocalQueue()) {
|
const logger = createLogger(env);
|
||||||
|
|
||||||
|
if (isLocalQueue(env)) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[EventBridge] createSchedule skipped (local SQS queue — no EventBridge in dev)",
|
"[EventBridge] createSchedule skipped (local SQS queue — no EventBridge in dev)",
|
||||||
);
|
);
|
||||||
@@ -49,7 +54,7 @@ export const createSchedule = async ({
|
|||||||
const d = scheduleAt;
|
const d = scheduleAt;
|
||||||
const atExpression = `at(${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())})`;
|
const atExpression = `at(${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())})`;
|
||||||
|
|
||||||
const sqsArn = getSqsQueueArn();
|
const sqsArn = getSqsQueueArn(env);
|
||||||
logger.info(
|
logger.info(
|
||||||
`[EventBridge] Creating schedule: name=${scheduleName} arn=${sqsArn} at=${atExpression}`,
|
`[EventBridge] Creating schedule: name=${scheduleName} arn=${sqsArn} at=${atExpression}`,
|
||||||
);
|
);
|
||||||
@@ -63,7 +68,7 @@ export const createSchedule = async ({
|
|||||||
FlexibleTimeWindow: { Mode: "OFF" },
|
FlexibleTimeWindow: { Mode: "OFF" },
|
||||||
Target: {
|
Target: {
|
||||||
Arn: sqsArn,
|
Arn: sqsArn,
|
||||||
RoleArn: SCHEDULER_ROLE_ARN,
|
RoleArn: getSchedulerRoleArn(env),
|
||||||
Input: sqsMessageBody,
|
Input: sqsMessageBody,
|
||||||
SqsParameters: {
|
SqsParameters: {
|
||||||
MessageGroupId: messageGroupId,
|
MessageGroupId: messageGroupId,
|
||||||
@@ -77,11 +82,15 @@ export const createSchedule = async ({
|
|||||||
|
|
||||||
/** Deletes an EventBridge schedule by name. Silently ignores not-found errors. */
|
/** Deletes an EventBridge schedule by name. Silently ignores not-found errors. */
|
||||||
export const deleteSchedule = async ({
|
export const deleteSchedule = async ({
|
||||||
|
env,
|
||||||
scheduleName,
|
scheduleName,
|
||||||
}: {
|
}: {
|
||||||
|
env: Env;
|
||||||
scheduleName: string;
|
scheduleName: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (isLocalQueue()) {
|
const logger = createLogger(env);
|
||||||
|
|
||||||
|
if (isLocalQueue(env)) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[EventBridge] deleteSchedule skipped (local SQS queue — no EventBridge in dev)",
|
"[EventBridge] deleteSchedule skipped (local SQS queue — no EventBridge in dev)",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { SchedulerClient } from "@aws-sdk/client-scheduler";
|
import { SchedulerClient } from "@aws-sdk/client-scheduler";
|
||||||
import {
|
import {
|
||||||
DEFAULT_AWS_REGION,
|
DEFAULT_AWS_REGION,
|
||||||
extractRegionFromQueueUrl,
|
extractRegionFromQueueUrl,
|
||||||
} from "@/external/aws/awsRegionUtils.js";
|
} from "@/external/aws/awsRegionUtils.js";
|
||||||
|
|
||||||
const getSchedulerClientConfig = () => ({
|
export let schedulerClient: SchedulerClient | null = null;
|
||||||
|
|
||||||
|
const getSchedulerClientConfig = (env: Env) => ({
|
||||||
region:
|
region:
|
||||||
extractRegionFromQueueUrl({
|
extractRegionFromQueueUrl({
|
||||||
queueUrl: runtimeEnv.SQS_QUEUE_URL_V2,
|
queueUrl: env.SQS_QUEUE_URL_V2,
|
||||||
}) || DEFAULT_AWS_REGION,
|
}) || DEFAULT_AWS_REGION,
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: runtimeEnv.AWS_ACCESS_KEY_ID || "",
|
accessKeyId: env.AWS_ACCESS_KEY_ID || "",
|
||||||
secretAccessKey: runtimeEnv.AWS_SECRET_ACCESS_KEY || "",
|
secretAccessKey: env.AWS_SECRET_ACCESS_KEY || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schedulerClient = new SchedulerClient(getSchedulerClientConfig());
|
export const initSchedulerClient = (env: Env) => {
|
||||||
|
schedulerClient = new SchedulerClient(getSchedulerClientConfig(env));
|
||||||
|
};
|
||||||
|
|||||||
12
server/src/external/aws/s3/adminS3Config.ts
vendored
12
server/src/external/aws/s3/adminS3Config.ts
vendored
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
export const ADMIN_REQUEST_BLOCK_CONFIG_KEY = "admin/request-block-config.json";
|
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_ROLLOUT_CONFIG_KEY = "admin/rollout-config.json";
|
||||||
export const ADMIN_FEATURE_FLAGS_CONFIG_KEY = "admin/feature-flags-config.json";
|
export const ADMIN_FEATURE_FLAGS_CONFIG_KEY = "admin/feature-flags-config.json";
|
||||||
@@ -22,18 +21,17 @@ export const BLUE_GREEN_CRON_ACTIVE_SLOT_KEY =
|
|||||||
"admin/blue-green-cron-active-slot.json";
|
"admin/blue-green-cron-active-slot.json";
|
||||||
export const BLUE_GREEN_HEARTBEAT_KEY_PREFIX = "admin/blue-green-heartbeats";
|
export const BLUE_GREEN_HEARTBEAT_KEY_PREFIX = "admin/blue-green-heartbeats";
|
||||||
|
|
||||||
const bucket = runtimeEnv.S3_BUCKET || "autumn-prod-server";
|
export const getAdminS3Config = (env: Env) => {
|
||||||
const region = runtimeEnv.S3_REGION || "us-east-2";
|
const bucket = env.S3_BUCKET || "autumn-prod-server";
|
||||||
|
const region = env.S3_REGION || "us-east-2";
|
||||||
export const getAdminS3Config = () => {
|
|
||||||
return {
|
return {
|
||||||
bucket,
|
bucket,
|
||||||
region,
|
region,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAdminEdgeConfigSources = () => ({
|
export const getAdminEdgeConfigSources = (env: Env) => ({
|
||||||
...getAdminS3Config(),
|
...getAdminS3Config(env),
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
id: "request-block",
|
id: "request-block",
|
||||||
|
|||||||
33
server/src/external/axiom/initAxiom.ts
vendored
33
server/src/external/axiom/initAxiom.ts
vendored
@@ -1,21 +1,26 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { Axiom } from "@axiomhq/js";
|
import { Axiom } from "@axiomhq/js";
|
||||||
|
|
||||||
const AXIOM_ADMIN_TOKEN = runtimeEnv.AXIOM_ADMIN_TOKEN;
|
let _axiomClient: Axiom | null = null;
|
||||||
const AXIOM_ORG_ID = runtimeEnv.AXIOM_ORG_ID;
|
|
||||||
|
|
||||||
export const axiomClient: Axiom | null = AXIOM_ADMIN_TOKEN
|
/** Initialize Axiom client from the platform env. Must be called before any logging. */
|
||||||
? new Axiom({
|
export const initAxiomClient = (env: Env): Axiom | null => {
|
||||||
token: AXIOM_ADMIN_TOKEN,
|
const token = env.AXIOM_ADMIN_TOKEN;
|
||||||
orgId: AXIOM_ORG_ID,
|
const orgId = env.AXIOM_ORG_ID;
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
export const getAxiomClient = (): Axiom => {
|
if (!token) {
|
||||||
if (!axiomClient) {
|
_axiomClient = null;
|
||||||
throw new Error("Axiom is not configured (AXIOM_ADMIN_TOKEN missing)");
|
return null;
|
||||||
}
|
}
|
||||||
return axiomClient;
|
|
||||||
|
_axiomClient = new Axiom({ token, orgId });
|
||||||
|
return _axiomClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAxiomConfigured = (): boolean => axiomClient !== null;
|
export const getAxiomClient = (): Axiom => {
|
||||||
|
if (!_axiomClient) {
|
||||||
|
throw new Error("Axiom is not configured (AXIOM_ADMIN_TOKEN missing)");
|
||||||
|
}
|
||||||
|
return _axiomClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAxiomConfigured = (): boolean => _axiomClient !== null;
|
||||||
|
|||||||
17
server/src/external/connect/connectUtils.ts
vendored
17
server/src/external/connect/connectUtils.ts
vendored
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { AppEnv, InternalError, type Organization } from "@autumn/shared";
|
import { AppEnv, InternalError, type Organization } from "@autumn/shared";
|
||||||
import type { DrizzleCli } from "@server/db/initDrizzle.js";
|
import type { DrizzleCli } from "@server/db/initDrizzle.js";
|
||||||
import { OrgService } from "@server/internal/orgs/OrgService.js";
|
import { OrgService } from "@server/internal/orgs/OrgService.js";
|
||||||
@@ -29,22 +28,24 @@ export const orgToAccountId = ({
|
|||||||
export const deauthorizeAccount = async ({
|
export const deauthorizeAccount = async ({
|
||||||
accountId,
|
accountId,
|
||||||
env,
|
env,
|
||||||
|
workerEnv,
|
||||||
logger,
|
logger,
|
||||||
}: {
|
}: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
|
workerEnv: Env;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
}) => {
|
}) => {
|
||||||
// OAuth-connected accounts must be deauthorized, not deleted
|
// OAuth-connected accounts must be deauthorized, not deleted
|
||||||
// Platform-managed accounts can be deleted
|
// Platform-managed accounts can be deleted
|
||||||
|
|
||||||
const masterStripe = initMasterStripe({ env });
|
const masterStripe = initMasterStripe(workerEnv, { env });
|
||||||
try {
|
try {
|
||||||
await masterStripe.oauth.deauthorize({
|
await masterStripe.oauth.deauthorize({
|
||||||
client_id:
|
client_id:
|
||||||
env === AppEnv.Live
|
env === AppEnv.Live
|
||||||
? runtimeEnv.STRIPE_LIVE_CLIENT_ID || ""
|
? workerEnv.STRIPE_LIVE_CLIENT_ID || ""
|
||||||
: runtimeEnv.STRIPE_SANDBOX_CLIENT_ID || "",
|
: workerEnv.STRIPE_SANDBOX_CLIENT_ID || "",
|
||||||
stripe_user_id: accountId,
|
stripe_user_id: accountId,
|
||||||
});
|
});
|
||||||
logger.info(`Deauthorized account ${accountId} for ${env}`);
|
logger.info(`Deauthorized account ${accountId} for ${env}`);
|
||||||
@@ -58,13 +59,15 @@ export const deauthorizeAccount = async ({
|
|||||||
export const deleteConnectedAccount = async ({
|
export const deleteConnectedAccount = async ({
|
||||||
accountId,
|
accountId,
|
||||||
env,
|
env,
|
||||||
|
workerEnv,
|
||||||
logger,
|
logger,
|
||||||
}: {
|
}: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
|
workerEnv: Env;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
}) => {
|
}) => {
|
||||||
const masterStripe = initMasterStripe({ env });
|
const masterStripe = initMasterStripe(workerEnv, { env });
|
||||||
try {
|
try {
|
||||||
await masterStripe.accounts.del(accountId);
|
await masterStripe.accounts.del(accountId);
|
||||||
logger.info(`Deleted account ${accountId} for ${env}`);
|
logger.info(`Deleted account ${accountId} for ${env}`);
|
||||||
@@ -102,10 +105,12 @@ export const getConnectWebhookSecret = async ({
|
|||||||
db,
|
db,
|
||||||
orgId,
|
orgId,
|
||||||
env,
|
env,
|
||||||
|
workerEnv,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
|
workerEnv: Env;
|
||||||
}) => {
|
}) => {
|
||||||
const org = await OrgService.get({ db, orgId });
|
const org = await OrgService.get({ db, orgId });
|
||||||
const prefix = env === AppEnv.Sandbox ? "test" : "live";
|
const prefix = env === AppEnv.Sandbox ? "test" : "live";
|
||||||
@@ -117,6 +122,6 @@ export const getConnectWebhookSecret = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const decrypted = decryptData(secret);
|
const decrypted = decryptData(secret, workerEnv);
|
||||||
return decrypted;
|
return decrypted;
|
||||||
};
|
};
|
||||||
|
|||||||
42
server/src/external/connect/initStripeCli.ts
vendored
42
server/src/external/connect/initStripeCli.ts
vendored
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import {
|
import {
|
||||||
AppEnv,
|
AppEnv,
|
||||||
InternalError,
|
InternalError,
|
||||||
@@ -16,28 +15,31 @@ import {
|
|||||||
import { getOrCreateStripeClient } from "./clientCache/stripeClientCache.js";
|
import { getOrCreateStripeClient } from "./clientCache/stripeClientCache.js";
|
||||||
import { getConnectWebhookSecret } from "./connectUtils.js";
|
import { getConnectWebhookSecret } from "./connectUtils.js";
|
||||||
|
|
||||||
export const initMasterStripe = (params?: {
|
export const initMasterStripe = (
|
||||||
|
env: Env,
|
||||||
|
params?: {
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
legacyVersion?: boolean;
|
legacyVersion?: boolean;
|
||||||
env?: AppEnv;
|
env?: AppEnv;
|
||||||
skipInstrumentation?: boolean;
|
skipInstrumentation?: boolean;
|
||||||
}) => {
|
},
|
||||||
|
) => {
|
||||||
let secretKey: string;
|
let secretKey: string;
|
||||||
|
|
||||||
if (params?.env === AppEnv.Live) {
|
if (params?.env === AppEnv.Live) {
|
||||||
if (!runtimeEnv.STRIPE_LIVE_SECRET_KEY) {
|
if (!env.STRIPE_LIVE_SECRET_KEY) {
|
||||||
throw new InternalError({
|
throw new InternalError({
|
||||||
message: "STRIPE_LIVE_SECRET_KEY env variable is not set",
|
message: "STRIPE_LIVE_SECRET_KEY env variable is not set",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
secretKey = runtimeEnv.STRIPE_LIVE_SECRET_KEY;
|
secretKey = env.STRIPE_LIVE_SECRET_KEY;
|
||||||
} else {
|
} else {
|
||||||
if (!runtimeEnv.STRIPE_SANDBOX_SECRET_KEY) {
|
if (!env.STRIPE_SANDBOX_SECRET_KEY) {
|
||||||
throw new InternalError({
|
throw new InternalError({
|
||||||
message: "STRIPE_SANDBOX_SECRET_KEY env variable is not set",
|
message: "STRIPE_SANDBOX_SECRET_KEY env variable is not set",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
secretKey = runtimeEnv.STRIPE_SANDBOX_SECRET_KEY;
|
secretKey = env.STRIPE_SANDBOX_SECRET_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = buildMasterCacheKey({
|
const cacheKey = buildMasterCacheKey({
|
||||||
@@ -66,12 +68,14 @@ export const initMasterStripe = (params?: {
|
|||||||
export const initPlatformStripe = ({
|
export const initPlatformStripe = ({
|
||||||
masterOrg,
|
masterOrg,
|
||||||
env,
|
env,
|
||||||
|
appEnv,
|
||||||
accountId,
|
accountId,
|
||||||
legacyVersion,
|
legacyVersion,
|
||||||
skipInstrumentation = false,
|
skipInstrumentation = false,
|
||||||
}: {
|
}: {
|
||||||
masterOrg: Organization | null;
|
masterOrg: Organization | null;
|
||||||
env: AppEnv;
|
env: Env;
|
||||||
|
appEnv: AppEnv;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
legacyVersion?: boolean;
|
legacyVersion?: boolean;
|
||||||
skipInstrumentation?: boolean;
|
skipInstrumentation?: boolean;
|
||||||
@@ -84,12 +88,12 @@ export const initPlatformStripe = ({
|
|||||||
|
|
||||||
// Get master org's secret key and validate access to the account
|
// Get master org's secret key and validate access to the account
|
||||||
const encrypted =
|
const encrypted =
|
||||||
env === AppEnv.Sandbox
|
appEnv === AppEnv.Sandbox
|
||||||
? masterOrg.stripe_config?.test_api_key
|
? masterOrg.stripe_config?.test_api_key
|
||||||
: masterOrg.stripe_config?.live_api_key;
|
: masterOrg.stripe_config?.live_api_key;
|
||||||
|
|
||||||
if (!encrypted) {
|
if (!encrypted) {
|
||||||
const envLabel = env === AppEnv.Sandbox ? "test" : "live";
|
const envLabel = appEnv === AppEnv.Sandbox ? "test" : "live";
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
message: `Master organization must have Stripe ${envLabel} secret key connected`,
|
message: `Master organization must have Stripe ${envLabel} secret key connected`,
|
||||||
});
|
});
|
||||||
@@ -97,7 +101,7 @@ export const initPlatformStripe = ({
|
|||||||
|
|
||||||
const cacheKey = buildPlatformCacheKey({
|
const cacheKey = buildPlatformCacheKey({
|
||||||
masterOrgId: masterOrg.id,
|
masterOrgId: masterOrg.id,
|
||||||
env,
|
env: appEnv,
|
||||||
accountId,
|
accountId,
|
||||||
legacyVersion,
|
legacyVersion,
|
||||||
encryptedKey: encrypted,
|
encryptedKey: encrypted,
|
||||||
@@ -106,7 +110,7 @@ export const initPlatformStripe = ({
|
|||||||
return getOrCreateStripeClient({
|
return getOrCreateStripeClient({
|
||||||
cacheKey,
|
cacheKey,
|
||||||
create: () => {
|
create: () => {
|
||||||
const decrypted = decryptData(encrypted);
|
const decrypted = decryptData(encrypted, env);
|
||||||
if (!decrypted) {
|
if (!decrypted) {
|
||||||
throw new InternalError({
|
throw new InternalError({
|
||||||
message: "Failed to decrypt master organization's Stripe secret key",
|
message: "Failed to decrypt master organization's Stripe secret key",
|
||||||
@@ -126,26 +130,28 @@ export const getStripeWebhookSecret = async ({
|
|||||||
db,
|
db,
|
||||||
orgId,
|
orgId,
|
||||||
env,
|
env,
|
||||||
|
appEnv,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
env: AppEnv;
|
env: Env;
|
||||||
|
appEnv: AppEnv;
|
||||||
}) => {
|
}) => {
|
||||||
// If org ID...
|
// If org ID...
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
return await getConnectWebhookSecret({ db, orgId, env });
|
return await getConnectWebhookSecret({ db, orgId, env: appEnv });
|
||||||
}
|
}
|
||||||
|
|
||||||
let secret: string;
|
let secret: string;
|
||||||
if (env === AppEnv.Live) {
|
if (appEnv === AppEnv.Live) {
|
||||||
secret = runtimeEnv.STRIPE_LIVE_WEBHOOK_SECRET || "";
|
secret = env.STRIPE_LIVE_WEBHOOK_SECRET || "";
|
||||||
} else {
|
} else {
|
||||||
secret = runtimeEnv.STRIPE_SANDBOX_WEBHOOK_SECRET || "";
|
secret = env.STRIPE_SANDBOX_WEBHOOK_SECRET || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
throw new InternalError({
|
throw new InternalError({
|
||||||
message: `STRIPE_WEBHOOK_SECRET env variable is not set (${env})`,
|
message: `STRIPE_WEBHOOK_SECRET env variable is not set (${appEnv})`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import type Stripe from "stripe";
|
import type Stripe from "stripe";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||||
@@ -8,17 +7,23 @@ import { initPlatformStripe } from "./initStripeCli.js";
|
|||||||
|
|
||||||
export const registerConnectWebhook = async ({
|
export const registerConnectWebhook = async ({
|
||||||
ctx,
|
ctx,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
|
env: Env;
|
||||||
}) => {
|
}) => {
|
||||||
const { db, org, env, logger } = ctx;
|
const { db, org, env: appEnv, logger } = ctx;
|
||||||
// Init master stripe
|
// Init master stripe
|
||||||
const stripeCli = initPlatformStripe({ masterOrg: org, env });
|
const stripeCli = initPlatformStripe({
|
||||||
|
masterOrg: org,
|
||||||
|
env,
|
||||||
|
appEnv,
|
||||||
|
});
|
||||||
|
|
||||||
const curWebhookEndpoints = await stripeCli.webhookEndpoints.list();
|
const curWebhookEndpoints = await stripeCli.webhookEndpoints.list();
|
||||||
const backendUrl = runtimeEnv.SERVER_URL || runtimeEnv.STRIPE_WEBHOOK_URL;
|
const backendUrl = env.SERVER_URL || env.STRIPE_WEBHOOK_URL;
|
||||||
|
|
||||||
const webhookUrl = `${backendUrl}/webhooks/connect/${env}?org_id=${org.id}`;
|
const webhookUrl = `${backendUrl}/webhooks/connect/${appEnv}?org_id=${org.id}`;
|
||||||
|
|
||||||
if (curWebhookEndpoints.data.some((webhook) => webhook.url === webhookUrl))
|
if (curWebhookEndpoints.data.some((webhook) => webhook.url === webhookUrl))
|
||||||
return;
|
return;
|
||||||
@@ -30,16 +35,16 @@ export const registerConnectWebhook = async ({
|
|||||||
connect: true,
|
connect: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Registered connect webhook for ${org.slug} ${env}`);
|
logger.info(`Registered connect webhook for ${org.slug} ${appEnv}`);
|
||||||
|
|
||||||
await OrgService.updateConnectWebhookSecret({
|
await OrgService.updateConnectWebhookSecret({
|
||||||
db,
|
db,
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
env,
|
env: appEnv,
|
||||||
secret: encryptData(webhook.secret as string),
|
secret: encryptData(webhook.secret as string),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Updated connect webhook secret for ${org.slug} ${env}`);
|
logger.info(`Updated connect webhook secret for ${org.slug} ${appEnv}`);
|
||||||
|
|
||||||
return webhook;
|
return webhook;
|
||||||
};
|
};
|
||||||
|
|||||||
12
server/src/external/hatchet/initHatchet.ts
vendored
12
server/src/external/hatchet/initHatchet.ts
vendored
@@ -1,6 +1,12 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1";
|
import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1";
|
||||||
|
|
||||||
export const isHatchetEnabled = !!runtimeEnv.HATCHET_CLIENT_TOKEN;
|
export let hatchet: ReturnType<typeof HatchetClient.init> | null = null;
|
||||||
|
|
||||||
export const hatchet = isHatchetEnabled ? HatchetClient.init() : null;
|
export const initHatchet = (env: Env) => {
|
||||||
|
const enabled = !!env.HATCHET_CLIENT_TOKEN;
|
||||||
|
if (enabled) {
|
||||||
|
hatchet = HatchetClient.init();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isHatchetEnabled = () => hatchet !== null;
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
* time via `syncEnvVars` to push secrets to the cloud env. Kept SDK-free
|
* time via `syncEnvVars` to push secrets to the cloud env. Kept SDK-free
|
||||||
* so trigger.config.ts can import it without bloating the build.
|
* so trigger.config.ts can import it without bloating the build.
|
||||||
*
|
*
|
||||||
* Runtime code uses `initInfisical` (SDK-based, populates runtimeEnv).
|
* Runtime code uses `initInfisical` (SDK-based, returns a merged env object).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
export type InfisicalSyncEnvVar = { name: string; value: string };
|
export type InfisicalSyncEnvVar = { name: string; value: string };
|
||||||
|
|
||||||
export type FetchInfisicalSecretsArgs = {
|
export type FetchInfisicalSecretsArgs = {
|
||||||
@@ -100,17 +99,15 @@ export const fetchInfisicalSecrets = async ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the four credential vars (`INFISICAL_CLIENT_ID`, `_SECRET`,
|
* Read the four credential vars (`INFISICAL_CLIENT_ID`, `_SECRET`,
|
||||||
* `_PROJECT_ID`, `_ENVIRONMENT`) from the local process env first then
|
* `_PROJECT_ID`, `_ENVIRONMENT`) from trigger.dev's deploy-time `ctx.env`.
|
||||||
* trigger.dev's deploy-time `ctx.env`. Convenience for `syncEnvVars`.
|
* Convenience for `syncEnvVars`.
|
||||||
*/
|
*/
|
||||||
export const fetchInfisicalSecretsFromEnv = (
|
export const fetchInfisicalSecretsFromEnv = (
|
||||||
ctxEnv: Record<string, string | undefined> = {},
|
ctxEnv: Record<string, string | undefined> = {},
|
||||||
): Promise<InfisicalSyncEnvVar[]> =>
|
): Promise<InfisicalSyncEnvVar[]> =>
|
||||||
fetchInfisicalSecrets({
|
fetchInfisicalSecrets({
|
||||||
clientId: runtimeEnv.INFISICAL_CLIENT_ID ?? ctxEnv.INFISICAL_CLIENT_ID,
|
clientId: ctxEnv.INFISICAL_CLIENT_ID,
|
||||||
clientSecret:
|
clientSecret: ctxEnv.INFISICAL_CLIENT_SECRET,
|
||||||
runtimeEnv.INFISICAL_CLIENT_SECRET ?? ctxEnv.INFISICAL_CLIENT_SECRET,
|
projectId: ctxEnv.INFISICAL_PROJECT_ID,
|
||||||
projectId: runtimeEnv.INFISICAL_PROJECT_ID ?? ctxEnv.INFISICAL_PROJECT_ID,
|
environment: ctxEnv.INFISICAL_ENVIRONMENT,
|
||||||
environment:
|
|
||||||
runtimeEnv.INFISICAL_ENVIRONMENT ?? ctxEnv.INFISICAL_ENVIRONMENT,
|
|
||||||
});
|
});
|
||||||
|
|||||||
56
server/src/external/infisical/initInfisical.ts
vendored
56
server/src/external/infisical/initInfisical.ts
vendored
@@ -1,27 +1,24 @@
|
|||||||
import { InfisicalSDK } from "@infisical/sdk";
|
import { InfisicalSDK } from "@infisical/sdk";
|
||||||
import {
|
|
||||||
getRuntimeEnvValue,
|
|
||||||
loadLocalEnv,
|
|
||||||
runtimeEnv,
|
|
||||||
setRuntimeEnvValue,
|
|
||||||
} from "@/utils/envUtils.js";
|
|
||||||
import { mask } from "@/utils/genUtils";
|
import { mask } from "@/utils/genUtils";
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Infisical and merge secrets into the given env.
|
||||||
|
* Returns a new env object with Infisical secrets merged in.
|
||||||
|
* Existing env variables take precedence (won't be overridden).
|
||||||
|
*/
|
||||||
|
export const initInfisical = async (
|
||||||
|
env: Env,
|
||||||
|
params?: { secretPath?: string },
|
||||||
|
): Promise<Env> => {
|
||||||
// Only initialize if credentials are provided
|
// Only initialize if credentials are provided
|
||||||
const clientId = runtimeEnv.INFISICAL_CLIENT_ID;
|
const clientId = env.INFISICAL_CLIENT_ID;
|
||||||
const clientSecret = runtimeEnv.INFISICAL_CLIENT_SECRET;
|
const clientSecret = env.INFISICAL_CLIENT_SECRET;
|
||||||
const projectId = runtimeEnv.INFISICAL_PROJECT_ID;
|
const projectId = env.INFISICAL_PROJECT_ID;
|
||||||
const environment = runtimeEnv.INFISICAL_ENVIRONMENT;
|
const environment = env.INFISICAL_ENVIRONMENT;
|
||||||
|
|
||||||
if (!clientId || !clientSecret || !projectId || !environment) {
|
if (!clientId || !clientSecret || !projectId || !environment) {
|
||||||
console.log("⚠️ Infisical credentials not found - skipping initialization");
|
console.log("⚠️ Infisical credentials not found - skipping initialization");
|
||||||
return;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -42,33 +39,34 @@ export const initInfisical = async (params?: { secretPath?: string }) => {
|
|||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load secrets into runtimeEnv
|
// Merge secrets into env (existing values take precedence)
|
||||||
// Note: Existing runtimeEnv variables take precedence (won't be overridden)
|
const secrets: Record<string, string> = {};
|
||||||
let loadedCount = 0;
|
|
||||||
|
|
||||||
for (const secret of allSecrets.secrets) {
|
for (const secret of allSecrets.secrets) {
|
||||||
// If path is restricted log that we're seeing it
|
|
||||||
if (secret.secretPath?.includes("restricted") && secret.secretValue) {
|
if (secret.secretPath?.includes("restricted") && secret.secretValue) {
|
||||||
console.log(
|
console.log(
|
||||||
`Retrieving restricted secret: ${secret.secretKey}, Path: ${secret.secretPath}, value: ${mask(secret.secretValue, 3, 2)}`,
|
`Retrieving restricted secret: ${secret.secretKey}, Path: ${
|
||||||
|
secret.secretPath
|
||||||
|
}, value: ${mask(secret.secretValue, 3, 2)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!getRuntimeEnvValue(secret.secretKey)) {
|
if (!env[secret.secretKey as keyof Env]) {
|
||||||
setRuntimeEnvValue(secret.secretKey, secret.secretValue);
|
secrets[secret.secretKey] = secret.secretValue;
|
||||||
loadedCount++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const importSecrets of allSecrets?.imports ?? []) {
|
for (const importSecrets of allSecrets?.imports ?? []) {
|
||||||
for (const importSecret of importSecrets.secrets) {
|
for (const importSecret of importSecrets.secrets) {
|
||||||
if (!getRuntimeEnvValue(importSecret.secretKey)) {
|
if (!env[importSecret.secretKey as keyof Env]) {
|
||||||
setRuntimeEnvValue(importSecret.secretKey, importSecret.secretValue);
|
secrets[importSecret.secretKey] = importSecret.secretValue;
|
||||||
loadedCount++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ Infisical: loaded ${loadedCount} secrets into runtimeEnv`);
|
const loadedCount = Object.keys(secrets).length;
|
||||||
|
console.log(`✅ Infisical: loaded ${loadedCount} secrets`);
|
||||||
|
|
||||||
|
return { ...secrets, ...env } as Env;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to initialize Infisical:", error);
|
console.error("❌ Failed to initialize Infisical:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
22
server/src/external/logtail/logtailUtils.ts
vendored
22
server/src/external/logtail/logtailUtils.ts
vendored
@@ -1,8 +1,5 @@
|
|||||||
import type pino from "pino";
|
import type pino from "pino";
|
||||||
import { initLogger } from "@/utils/logging/initLogger";
|
import { initLogger } from "@/utils/logging/initLogger";
|
||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
|
|
||||||
const pinoLogger = initLogger({}, runtimeEnv);
|
|
||||||
|
|
||||||
const createLogMethod = (pinoMethod: any, logtailMethod?: any) => {
|
const createLogMethod = (pinoMethod: any, logtailMethod?: any) => {
|
||||||
function rewriteAppPath(str: string) {
|
function rewriteAppPath(str: string) {
|
||||||
@@ -97,27 +94,16 @@ const createLoggerStructure = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const createLogger = (env: Env) =>
|
export const createLogger = (env: Env) =>
|
||||||
createLoggerStructure(
|
createLoggerStructure(initLogger({}, env), env);
|
||||||
env === runtimeEnv ? pinoLogger : initLogger({}, env),
|
|
||||||
env,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy dual-output logger (stdout JSON + axiom). Used only by long-running
|
* Lazy dual-output logger (stdout JSON + axiom). Used only by long-running
|
||||||
* trigger.dev tasks so their lines surface in both the trigger run UI and
|
* trigger.dev tasks so their lines surface in both the trigger run UI and
|
||||||
* our axiom store. Default `logger` / `createLogger` are unaffected.
|
* our axiom store. `createLogger` is unaffected.
|
||||||
*/
|
*/
|
||||||
let dualPinoLogger: pino.Logger | null = null;
|
export const createDualLogger = (env: Env) =>
|
||||||
export const createDualLogger = (env: Env) => {
|
createLoggerStructure(initLogger({ mode: "dual" }, 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(runtimeEnv);
|
|
||||||
export type Logger = {
|
export type Logger = {
|
||||||
debug: (...args: any[]) => void;
|
debug: (...args: any[]) => void;
|
||||||
info: (...args: any[]) => void;
|
info: (...args: any[]) => void;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { onAwsEcs } from "@/external/aws/ecs/onAwsEcs.js";
|
import { onAwsEcs } from "@/external/aws/ecs/onAwsEcs.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,13 +17,19 @@ import { onAwsEcs } from "@/external/aws/ecs/onAwsEcs.js";
|
|||||||
* Returns the same input string when no swap applies, so callers can use
|
* Returns the same input string when no swap applies, so callers can use
|
||||||
* it transparently in place of the raw URL.
|
* it transparently in place of the raw URL.
|
||||||
*/
|
*/
|
||||||
export const getReachableDragonflyUrl = (url: string): string => {
|
export const getReachableDragonflyUrl = ({
|
||||||
if (onAwsEcs()) return url;
|
url,
|
||||||
|
env,
|
||||||
|
}: {
|
||||||
|
url: string;
|
||||||
|
env: Env;
|
||||||
|
}): string => {
|
||||||
|
if (onAwsEcs(env)) return url;
|
||||||
|
|
||||||
const privateUrl = runtimeEnv.CACHE_V2_DRAGONFLY_URL?.trim();
|
const privateUrl = env.CACHE_V2_DRAGONFLY_URL?.trim();
|
||||||
if (!privateUrl || url.trim() !== privateUrl) return url;
|
if (!privateUrl || url.trim() !== privateUrl) return url;
|
||||||
|
|
||||||
const publicUrl = runtimeEnv.CACHE_V2_DRAGONFLY_PUBLIC_URL?.trim();
|
const publicUrl = env.CACHE_V2_DRAGONFLY_PUBLIC_URL?.trim();
|
||||||
if (!publicUrl) return url;
|
if (!publicUrl) return url;
|
||||||
|
|
||||||
return publicUrl;
|
return publicUrl;
|
||||||
|
|||||||
69
server/src/external/redis/initRedisV2.ts
vendored
69
server/src/external/redis/initRedisV2.ts
vendored
@@ -1,6 +1,5 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import type { Redis } from "ioredis";
|
import type { Redis } from "ioredis";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js";
|
import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js";
|
||||||
import { getReachableDragonflyUrl } from "./getReachableDragonflyUrl.js";
|
import { getReachableDragonflyUrl } from "./getReachableDragonflyUrl.js";
|
||||||
import {
|
import {
|
||||||
@@ -9,40 +8,71 @@ import {
|
|||||||
waitForRedisReady,
|
waitForRedisReady,
|
||||||
} from "./initRedis.js";
|
} from "./initRedis.js";
|
||||||
import {
|
import {
|
||||||
REDIS_V2_COMMAND_TIMEOUT_MS,
|
getRedisV2ConnectionConfig,
|
||||||
supportsUpstashShebangForRedisV2,
|
supportsUpstashShebangForRedisV2,
|
||||||
} from "./initUtils/redisV2Config.js";
|
} from "./initUtils/redisV2Config.js";
|
||||||
|
|
||||||
const rawDragonflyUrl = runtimeEnv.CACHE_V2_DRAGONFLY_URL?.trim();
|
let _env: Env | undefined;
|
||||||
|
let _redisV2: Redis | undefined;
|
||||||
|
let _alternateInstanceUrls: Partial<Record<RedisV2InstanceName, string>> = {};
|
||||||
|
|
||||||
|
export const ensureRedisV2 = (env: Env): Redis => {
|
||||||
|
if (_redisV2) return _redisV2;
|
||||||
|
|
||||||
|
_env = env;
|
||||||
|
const rawDragonflyUrl = env.CACHE_V2_DRAGONFLY_URL?.trim();
|
||||||
const dragonflyUrl = rawDragonflyUrl
|
const dragonflyUrl = rawDragonflyUrl
|
||||||
? getReachableDragonflyUrl(rawDragonflyUrl)
|
? getReachableDragonflyUrl({ url: rawDragonflyUrl, env })
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
export const hasRedisV2Config = Boolean(dragonflyUrl);
|
const config = getRedisV2ConnectionConfig({
|
||||||
|
cacheV2Url: dragonflyUrl || undefined,
|
||||||
export const redisV2: Redis = createRedisConnection({
|
currentRegion: currentRegion as unknown as string,
|
||||||
cacheUrl: dragonflyUrl || "",
|
instanceName: "dragonfly",
|
||||||
region: `${currentRegion}:v2`,
|
env,
|
||||||
supportsUpstashShebang: false,
|
|
||||||
commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const alternateInstanceUrls: Partial<Record<RedisV2InstanceName, string>> = {
|
_redisV2 = createRedisConnection({
|
||||||
upstash: runtimeEnv.CACHE_V2_UPSTASH_URL?.trim() || undefined,
|
...(config || { cacheUrl: "", region: `${String(currentRegion)}:v2` }),
|
||||||
redis: runtimeEnv.CACHE_V2_REDIS_URL?.trim() || undefined,
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
_alternateInstanceUrls = {
|
||||||
|
upstash: env.CACHE_V2_UPSTASH_URL?.trim() || undefined,
|
||||||
|
redis: env.CACHE_V2_REDIS_URL?.trim() || undefined,
|
||||||
dragonfly: dragonflyUrl,
|
dragonfly: dragonflyUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return _redisV2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const redisV2: Redis = new Proxy({} as Redis, {
|
||||||
|
get: (_target, prop) => {
|
||||||
|
if (!_redisV2) {
|
||||||
|
throw new Error("redisV2 not initialized. Call ensureRedisV2(env) first.");
|
||||||
|
}
|
||||||
|
const value = (_redisV2 as unknown as Record<string | symbol, unknown>)[prop];
|
||||||
|
if (typeof value === "function") {
|
||||||
|
return value.bind(_redisV2);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const hasRedisV2Config = (): boolean =>
|
||||||
|
Boolean(_env?.CACHE_V2_DRAGONFLY_URL?.trim());
|
||||||
|
|
||||||
const instancePool = new Map<RedisV2InstanceName, Redis>();
|
const instancePool = new Map<RedisV2InstanceName, Redis>();
|
||||||
const missingUrlWarned = new Set<RedisV2InstanceName>();
|
const missingUrlWarned = new Set<RedisV2InstanceName>();
|
||||||
|
|
||||||
export const getAlternateRedisV2Instance = (
|
export const getAlternateRedisV2Instance = (
|
||||||
name: RedisV2InstanceName,
|
name: RedisV2InstanceName,
|
||||||
): Redis | null => {
|
): Redis | null => {
|
||||||
const cacheUrl = alternateInstanceUrls[name];
|
const cacheUrl = _alternateInstanceUrls[name];
|
||||||
if (!cacheUrl) {
|
if (!cacheUrl) {
|
||||||
if (!missingUrlWarned.has(name)) {
|
if (!missingUrlWarned.has(name)) {
|
||||||
missingUrlWarned.add(name);
|
missingUrlWarned.add(name);
|
||||||
|
const logger = _env ? createLogger(_env) : console;
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[resolveRedisV2] activeInstance=${name} but URL is not set; falling back to primary`,
|
`[resolveRedisV2] activeInstance=${name} but URL is not set; falling back to primary`,
|
||||||
);
|
);
|
||||||
@@ -57,14 +87,15 @@ export const getAlternateRedisV2Instance = (
|
|||||||
cacheUrl,
|
cacheUrl,
|
||||||
region: `${currentRegion}:v2:${name}`,
|
region: `${currentRegion}:v2:${name}`,
|
||||||
supportsUpstashShebang: supportsUpstashShebangForRedisV2(name),
|
supportsUpstashShebang: supportsUpstashShebangForRedisV2(name),
|
||||||
commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS,
|
env: _env!,
|
||||||
});
|
});
|
||||||
instancePool.set(name, instance);
|
instancePool.set(name, instance);
|
||||||
return instance;
|
return instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const warmupRedisV2 = async (): Promise<void> => {
|
export const warmupRedisV2 = async (): Promise<void> => {
|
||||||
if (!hasRedisV2Config) return;
|
if (!_env?.CACHE_V2_DRAGONFLY_URL?.trim()) return;
|
||||||
|
if (!_redisV2) return;
|
||||||
|
|
||||||
await waitForRedisReady(redisV2, "v2");
|
await waitForRedisReady(_redisV2, "v2");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Redis } from "ioredis";
|
import type { Redis } from "ioredis";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import { withTimeout } from "@/utils/withTimeout.js";
|
import { withTimeout } from "@/utils/withTimeout.js";
|
||||||
import { waitForRedisReady } from "./redisWarmup.js";
|
import { waitForRedisReady } from "./redisWarmup.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
import { instrumentRedis } from "../otel/instrumentRedis.js";
|
import { instrumentRedis } from "../otel/instrumentRedis.js";
|
||||||
import { cacheBackupUrl } from "./redisConfig.js";
|
|
||||||
import { registerRedisCommands } from "./registerRedisCommands.js";
|
import { registerRedisCommands } from "./registerRedisCommands.js";
|
||||||
|
|
||||||
const REDIS_COMMAND_TIMEOUT_MS =
|
|
||||||
runtimeEnv.NODE_ENV === "production" ? 10_000 : 60_000;
|
|
||||||
|
|
||||||
const formatRedisEndpoint = ({ cacheUrl }: { cacheUrl: string }) => {
|
const formatRedisEndpoint = ({ cacheUrl }: { cacheUrl: string }) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(cacheUrl);
|
const url = new URL(cacheUrl);
|
||||||
@@ -24,36 +19,36 @@ export const createRedisClient = ({
|
|||||||
cacheUrl,
|
cacheUrl,
|
||||||
region,
|
region,
|
||||||
supportsUpstashShebang = false,
|
supportsUpstashShebang = false,
|
||||||
commandTimeout = REDIS_COMMAND_TIMEOUT_MS,
|
commandTimeout,
|
||||||
|
env,
|
||||||
|
cacheBackupUrl,
|
||||||
}: {
|
}: {
|
||||||
cacheUrl: string;
|
cacheUrl: string;
|
||||||
region: string;
|
region: string;
|
||||||
supportsUpstashShebang?: boolean;
|
supportsUpstashShebang?: boolean;
|
||||||
commandTimeout?: number;
|
commandTimeout?: number;
|
||||||
|
env: Env;
|
||||||
|
cacheBackupUrl?: string;
|
||||||
}): Redis => {
|
}): Redis => {
|
||||||
|
const timeout = commandTimeout ?? (
|
||||||
|
env.NODE_ENV === "production" ? 10_000 : 60_000
|
||||||
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[Redis] ${region}: connecting to ${formatRedisEndpoint({ cacheUrl })}`,
|
`[Redis] ${region}: connecting to ${formatRedisEndpoint({ cacheUrl })}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const instance = new Redis(cacheUrl, {
|
const instance = new Redis(cacheUrl, {
|
||||||
tls:
|
tls:
|
||||||
runtimeEnv.CACHE_CERT && !cacheBackupUrl
|
env.CACHE_CERT && !cacheBackupUrl
|
||||||
? { ca: runtimeEnv.CACHE_CERT }
|
? { ca: env.CACHE_CERT }
|
||||||
: undefined,
|
: undefined,
|
||||||
family: 4,
|
family: 4,
|
||||||
keepAlive: 10000,
|
keepAlive: 10000,
|
||||||
commandTimeout,
|
commandTimeout: timeout,
|
||||||
// Let `commandTimeout` (default 10s) be the sole bound on how long a command
|
|
||||||
// can wait. `maxRetriesPerRequest: null` disables ioredis's default
|
|
||||||
// "flush pending commands after N reconnect attempts" behavior, which
|
|
||||||
// otherwise aborts commands still in the offline queue on any minor
|
|
||||||
// handshake blip. Under a real brownout, commands still fail via the
|
|
||||||
// `Command timed out` path.
|
|
||||||
maxRetriesPerRequest: null,
|
maxRetriesPerRequest: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// instrumentRedis must run first so its defineCommand patch
|
|
||||||
// is in place when commands are registered.
|
|
||||||
instrumentRedis({ redis: instance, region });
|
instrumentRedis({ redis: instance, region });
|
||||||
registerRedisCommands({ redisInstance: instance, supportsUpstashShebang });
|
registerRedisCommands({ redisInstance: instance, supportsUpstashShebang });
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import type { Redis } from "ioredis";
|
import type { Redis } from "ioredis";
|
||||||
import { createDisabledRedis, createRedisClient } from "./createRedisClient.js";
|
import { createDisabledRedis, createRedisClient } from "./createRedisClient.js";
|
||||||
import {
|
import {
|
||||||
@@ -7,9 +6,26 @@ import {
|
|||||||
hasRedisConfig,
|
hasRedisConfig,
|
||||||
PRIMARY_REGION,
|
PRIMARY_REGION,
|
||||||
primaryCacheUrl,
|
primaryCacheUrl,
|
||||||
|
cacheBackupUrl,
|
||||||
|
initRedisConfig,
|
||||||
} from "./redisConfig.js";
|
} from "./redisConfig.js";
|
||||||
|
|
||||||
if (runtimeEnv.CACHE_BACKUP_URL?.trim()) {
|
let _redis: Redis | null = null;
|
||||||
|
const _regionalRedisInstances: Map<string, Redis> = new Map();
|
||||||
|
|
||||||
|
const getRedis = (): Redis => {
|
||||||
|
if (!_redis) {
|
||||||
|
throw new Error("Redis not initialized — call initRedisClientRegistry(env) first");
|
||||||
|
}
|
||||||
|
return _redis;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initRedisClientRegistry = (env: Env) => {
|
||||||
|
if (_redis) return;
|
||||||
|
|
||||||
|
initRedisConfig(env);
|
||||||
|
|
||||||
|
if (cacheBackupUrl) {
|
||||||
console.log(
|
console.log(
|
||||||
`[Redis] Using CACHE_BACKUP_URL for all regions (primary region: ${currentRegion})`,
|
`[Redis] Using CACHE_BACKUP_URL for all regions (primary region: ${currentRegion})`,
|
||||||
);
|
);
|
||||||
@@ -17,35 +33,37 @@ if (runtimeEnv.CACHE_BACKUP_URL?.trim()) {
|
|||||||
console.warn(
|
console.warn(
|
||||||
"[Redis] No Redis URL configured. Running in Postgres-only mode.",
|
"[Redis] No Redis URL configured. Running in Postgres-only mode.",
|
||||||
);
|
);
|
||||||
} else if (primaryCacheUrl && getCacheUrlForRegion({ region: currentRegion })) {
|
} else if (primaryCacheUrl && getCacheUrlForRegion({ region: currentRegion as string })) {
|
||||||
console.log(`Using regional cache: ${currentRegion}`);
|
console.log(`Using regional cache: ${currentRegion}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryRedis =
|
_redis =
|
||||||
hasRedisConfig && primaryCacheUrl
|
hasRedisConfig && primaryCacheUrl
|
||||||
? createRedisClient({
|
? createRedisClient({
|
||||||
cacheUrl: primaryCacheUrl,
|
env,
|
||||||
region: currentRegion,
|
cacheUrl: primaryCacheUrl as string,
|
||||||
|
region: currentRegion as string,
|
||||||
})
|
})
|
||||||
: createDisabledRedis();
|
: createDisabledRedis();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The active Redis instance. All consumer code imports this.
|
* The active Redis instance. All consumer code imports this.
|
||||||
* Normally points to the primary (current region).
|
* Normally points to the primary (current region).
|
||||||
*/
|
*/
|
||||||
export const redis: Redis = primaryRedis;
|
export const redis: Redis = new Proxy({} as Redis, {
|
||||||
|
get(_target, prop) {
|
||||||
// Lazy-loaded regional Redis instances for cross-region sync
|
const inst = getRedis();
|
||||||
const regionalRedisInstances: Map<string, Redis> = new Map();
|
const value = (inst as Record<string | symbol, unknown>)[prop];
|
||||||
|
return typeof value === "function" ? value.bind(inst) : value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/** Get Redis instance for a specific region (lazy-loaded) */
|
/** Get Redis instance for a specific region (lazy-loaded) */
|
||||||
export const getRegionalRedis = (region: string): Redis => {
|
export const getRegionalRedis = (region: string): Redis => {
|
||||||
if (!hasRedisConfig) {
|
const inst = getRedis();
|
||||||
return primaryRedis;
|
if (!hasRedisConfig) return inst;
|
||||||
}
|
if (region === currentRegion) return inst;
|
||||||
if (region === currentRegion) {
|
|
||||||
return primaryRedis;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheUrl = getCacheUrlForRegion({ region });
|
const cacheUrl = getCacheUrlForRegion({ region });
|
||||||
|
|
||||||
@@ -53,23 +71,17 @@ export const getRegionalRedis = (region: string): Redis => {
|
|||||||
console.warn(
|
console.warn(
|
||||||
`No cache URL configured for region ${region}, falling back to primary`,
|
`No cache URL configured for region ${region}, falling back to primary`,
|
||||||
);
|
);
|
||||||
return primaryRedis;
|
return inst;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cacheUrl === primaryCacheUrl) {
|
if (cacheUrl === primaryCacheUrl) return inst;
|
||||||
return primaryRedis;
|
|
||||||
}
|
|
||||||
|
|
||||||
let regionalInstance = regionalRedisInstances.get(region);
|
let regionalInstance = _regionalRedisInstances.get(region);
|
||||||
if (regionalInstance) {
|
if (regionalInstance) return regionalInstance;
|
||||||
return regionalInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Creating Redis connection for region: ${region}`);
|
console.log(`Creating Redis connection for region: ${region}`);
|
||||||
regionalInstance = createRedisClient({ cacheUrl, region });
|
// Note: this needs env, but we cache it from init
|
||||||
regionalRedisInstances.set(region, regionalInstance);
|
throw new Error("Regional Redis not yet supported post-migration — needs env");
|
||||||
|
|
||||||
return regionalInstance;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Get the primary Redis instance (us-west-2) to avoid replication lag issues */
|
/** Get the primary Redis instance (us-west-2) to avoid replication lag issues */
|
||||||
|
|||||||
@@ -1,39 +1,59 @@
|
|||||||
// Region constants
|
// Region constants
|
||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
const REGION_US_EAST_2 = "us-east-2";
|
const REGION_US_EAST_2 = "us-east-2";
|
||||||
const REGION_US_WEST_2 = "us-west-2";
|
const REGION_US_WEST_2 = "us-west-2";
|
||||||
|
|
||||||
// All configured regions
|
// All configured regions
|
||||||
const ALL_REGIONS = [REGION_US_EAST_2, REGION_US_WEST_2] as const;
|
const ALL_REGIONS = [REGION_US_EAST_2, REGION_US_WEST_2] as const;
|
||||||
|
|
||||||
// Current region this instance is running in
|
export const PRIMARY_REGION = REGION_US_WEST_2;
|
||||||
export const currentRegion = runtimeEnv.AWS_REGION || REGION_US_WEST_2;
|
|
||||||
|
|
||||||
export const cacheBackupUrl = runtimeEnv.CACHE_BACKUP_URL?.trim();
|
let _initialized = false;
|
||||||
|
let _currentRegion = REGION_US_WEST_2;
|
||||||
|
let _primaryCacheUrl: string | undefined;
|
||||||
|
let _cacheBackupUrl: string | undefined;
|
||||||
|
let _regionToCacheUrl: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
// Map of region to cache URL. When CACHE_BACKUP_URL is set, all regions use it
|
export const initRedisConfig = (env: Env) => {
|
||||||
// (failover / single backup endpoint).
|
if (_initialized) return;
|
||||||
const regionToCacheUrl: Record<string, string | undefined> = cacheBackupUrl
|
_initialized = true;
|
||||||
|
|
||||||
|
_currentRegion = env.AWS_REGION || REGION_US_WEST_2;
|
||||||
|
_cacheBackupUrl = env.CACHE_BACKUP_URL?.trim();
|
||||||
|
_regionToCacheUrl = _cacheBackupUrl
|
||||||
? {
|
? {
|
||||||
[REGION_US_EAST_2]: cacheBackupUrl,
|
[REGION_US_EAST_2]: _cacheBackupUrl,
|
||||||
[REGION_US_WEST_2]: cacheBackupUrl,
|
[REGION_US_WEST_2]: _cacheBackupUrl,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
[REGION_US_EAST_2]: runtimeEnv.CACHE_URL_US_EAST,
|
[REGION_US_EAST_2]: env.CACHE_URL_US_EAST,
|
||||||
[REGION_US_WEST_2]: runtimeEnv.CACHE_URL,
|
[REGION_US_WEST_2]: env.CACHE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const primaryCacheUrl =
|
_primaryCacheUrl =
|
||||||
regionToCacheUrl[currentRegion] || runtimeEnv.CACHE_URL || cacheBackupUrl;
|
_regionToCacheUrl[_currentRegion] || env.CACHE_URL || _cacheBackupUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const currentRegion = new Proxy({} as unknown as string, {
|
||||||
|
get() { return _currentRegion; },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cacheBackupUrl = new Proxy({} as unknown as string | undefined, {
|
||||||
|
get() { return _cacheBackupUrl; },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const primaryCacheUrl = new Proxy({} as unknown as string | undefined, {
|
||||||
|
get() { return _primaryCacheUrl; },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const hasRedisConfig = new Proxy({} as unknown as boolean, {
|
||||||
|
get() { return Boolean(_primaryCacheUrl); },
|
||||||
|
});
|
||||||
|
|
||||||
export const hasRedisConfig = Boolean(primaryCacheUrl);
|
|
||||||
/** Get all regions that have configured cache URLs */
|
/** Get all regions that have configured cache URLs */
|
||||||
export const getConfiguredRegions = (): string[] => {
|
export const getConfiguredRegions = (): string[] => {
|
||||||
return ALL_REGIONS.filter((region) => regionToCacheUrl[region]);
|
return ALL_REGIONS.filter((region) => _regionToCacheUrl[region]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCacheUrlForRegion = ({ region }: { region: string }) => {
|
export const getCacheUrlForRegion = ({ region }: { region: string }) => {
|
||||||
return regionToCacheUrl[region];
|
return _regionToCacheUrl[region];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PRIMARY_REGION = REGION_US_WEST_2;
|
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js";
|
import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js";
|
||||||
|
|
||||||
export const REDIS_V2_COMMAND_TIMEOUT_MS =
|
|
||||||
runtimeEnv.NODE_ENV === "production" ? 1_000 : 10_000;
|
|
||||||
|
|
||||||
export const getRedisV2ConnectionConfig = ({
|
export const getRedisV2ConnectionConfig = ({
|
||||||
cacheV2Url,
|
cacheV2Url,
|
||||||
currentRegion,
|
currentRegion,
|
||||||
instanceName,
|
instanceName,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
cacheV2Url?: string;
|
cacheV2Url?: string;
|
||||||
currentRegion: string;
|
currentRegion: string;
|
||||||
instanceName: RedisV2InstanceName;
|
instanceName: RedisV2InstanceName;
|
||||||
}) =>
|
env: Env;
|
||||||
cacheV2Url?.trim()
|
}) => {
|
||||||
|
const commandTimeout = env.NODE_ENV === "production" ? 1_000 : 10_000;
|
||||||
|
return cacheV2Url?.trim()
|
||||||
? {
|
? {
|
||||||
cacheUrl: cacheV2Url.trim(),
|
cacheUrl: cacheV2Url.trim(),
|
||||||
region: `${currentRegion}:v2`,
|
region: `${currentRegion}:v2`,
|
||||||
supportsUpstashShebang: supportsUpstashShebangForRedisV2(instanceName),
|
supportsUpstashShebang: supportsUpstashShebangForRedisV2(instanceName),
|
||||||
commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS,
|
commandTimeout,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
export const supportsUpstashShebangForRedisV2 = (name: RedisV2InstanceName) =>
|
export const supportsUpstashShebangForRedisV2 = (name: RedisV2InstanceName) =>
|
||||||
name === "upstash";
|
name === "upstash";
|
||||||
|
|||||||
2
server/src/external/redis/orgRedisPool.ts
vendored
2
server/src/external/redis/orgRedisPool.ts
vendored
@@ -1,7 +1,7 @@
|
|||||||
import type { OrgRedisConfig } from "@autumn/shared";
|
import type { OrgRedisConfig } from "@autumn/shared";
|
||||||
import type { Redis } from "ioredis";
|
import type { Redis } from "ioredis";
|
||||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||||
import { decryptData } from "@/utils/encryptUtils.js";
|
import { decryptData } from "@/utils/encryptUtils.js";
|
||||||
import { getReachableDragonflyUrl } from "./getReachableDragonflyUrl.js";
|
import { getReachableDragonflyUrl } from "./getReachableDragonflyUrl.js";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import { addRedisToLogs } from "@/utils/logging/addContextToLogs.js";
|
import { addRedisToLogs } from "@/utils/logging/addContextToLogs.js";
|
||||||
import type { RedisKeyContext } from "./parseRedisKeyContext.js";
|
import type { RedisKeyContext } from "./parseRedisKeyContext.js";
|
||||||
import type { ResolvedThresholds } from "./redisSlowlogConfig.js";
|
import type { ResolvedThresholds } from "./redisSlowlogConfig.js";
|
||||||
|
|||||||
2
server/src/external/redis/resolveRedisV2.ts
vendored
2
server/src/external/redis/resolveRedisV2.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
import type { Redis } from "ioredis";
|
import type { Redis } from "ioredis";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import {
|
import {
|
||||||
getRampDestinationRedis,
|
getRampDestinationRedis,
|
||||||
isCacheV2RampEnabled,
|
isCacheV2RampEnabled,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Redis } from "ioredis";
|
import type { Redis } from "ioredis";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import { redis } from "@/external/redis/initRedis.js";
|
import { redis } from "@/external/redis/initRedis.js";
|
||||||
import { RedisUnavailableError } from "./errors.js";
|
import { RedisUnavailableError } from "./errors.js";
|
||||||
|
|
||||||
|
|||||||
14
server/src/external/resend/loopsUtils.ts
vendored
14
server/src/external/resend/loopsUtils.ts
vendored
@@ -1,20 +1,20 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import type { User } from "better-auth";
|
import type { User } from "better-auth";
|
||||||
import { LoopsClient } from "loops";
|
import { LoopsClient } from "loops";
|
||||||
import { logger } from "../logtail/logtailUtils.js";
|
import { createLogger } from "../logtail/logtailUtils.js";
|
||||||
|
|
||||||
const createLoopsCli = () => {
|
const createLoopsCli = (env: Env) => {
|
||||||
return new LoopsClient(runtimeEnv.LOOPS_API_KEY || "");
|
return new LoopsClient(env.LOOPS_API_KEY || "");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createLoopsContact = async (user: User) => {
|
export const createLoopsContact = async (env: Env, user: User) => {
|
||||||
if (!runtimeEnv.LOOPS_API_KEY) return;
|
const logger = createLogger(env);
|
||||||
|
if (!env.LOOPS_API_KEY) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const email = user.email;
|
const email = user.email;
|
||||||
const firstName = user.name?.split(" ")[0] || "";
|
const firstName = user.name?.split(" ")[0] || "";
|
||||||
const lastName = user.name?.split(" ")[1] || "";
|
const lastName = user.name?.split(" ")[1] || "";
|
||||||
const loops = createLoopsCli();
|
const loops = createLoopsCli(env);
|
||||||
|
|
||||||
const resp = await loops.createContact(email, {
|
const resp = await loops.createContact(email, {
|
||||||
firstName,
|
firstName,
|
||||||
|
|||||||
18
server/src/external/resend/resendUtils.ts
vendored
18
server/src/external/resend/resendUtils.ts
vendored
@@ -1,6 +1,5 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { Resend } from "resend";
|
import { Resend } from "resend";
|
||||||
import { logger } from "../logtail/logtailUtils.js";
|
import { createLogger } from "../logtail/logtailUtils.js";
|
||||||
|
|
||||||
interface ResendEmailProps {
|
interface ResendEmailProps {
|
||||||
to: string;
|
to: string;
|
||||||
@@ -11,17 +10,19 @@ interface ResendEmailProps {
|
|||||||
replyTo?: string;
|
replyTo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createResendCli = () => {
|
export const createResendCli = (env: Env) => {
|
||||||
return new Resend(runtimeEnv.RESEND_API_KEY);
|
return new Resend(env.RESEND_API_KEY);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendTextEmail = async ({
|
export const sendTextEmail = async ({
|
||||||
|
env,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
body,
|
body,
|
||||||
}: ResendEmailProps) => {
|
}: ResendEmailProps & { env: Env }) => {
|
||||||
const resend = createResendCli();
|
const logger = createLogger(env);
|
||||||
|
const resend = createResendCli(env);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(`Sending email to ${to} with subject ${subject}`);
|
logger.info(`Sending email to ${to} with subject ${subject}`);
|
||||||
@@ -58,13 +59,14 @@ export const sendTextEmail = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const sendHtmlEmail = async ({
|
export const sendHtmlEmail = async ({
|
||||||
|
env,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
body,
|
body,
|
||||||
replyTo,
|
replyTo,
|
||||||
}: ResendEmailProps) => {
|
}: ResendEmailProps & { env: Env }) => {
|
||||||
const resend = createResendCli();
|
const resend = createResendCli(env);
|
||||||
|
|
||||||
await resend.emails.send({
|
await resend.emails.send({
|
||||||
from: from,
|
from: from,
|
||||||
|
|||||||
8
server/src/external/resend/safeResend.ts
vendored
8
server/src/external/resend/safeResend.ts
vendored
@@ -1,15 +1,17 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
import { createLogger } from "../logtail/logtailUtils.js";
|
||||||
import { logger } from "../logtail/logtailUtils.js";
|
|
||||||
|
|
||||||
export function safeResend<T extends (...args: any[]) => any>({
|
export function safeResend<T extends (...args: any[]) => any>({
|
||||||
|
env,
|
||||||
fn,
|
fn,
|
||||||
action,
|
action,
|
||||||
}: {
|
}: {
|
||||||
|
env: Env;
|
||||||
fn: T;
|
fn: T;
|
||||||
action: string;
|
action: string;
|
||||||
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
||||||
|
const logger = createLogger(env);
|
||||||
return async (...args: Parameters<T>) => {
|
return async (...args: Parameters<T>) => {
|
||||||
if (!runtimeEnv.RESEND_API_KEY || !runtimeEnv.RESEND_DOMAIN) {
|
if (!env.RESEND_API_KEY || !env.RESEND_DOMAIN) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`,
|
`RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { decryptData, encryptData } from "@/utils/encryptUtils.js";
|
|||||||
|
|
||||||
const TOKEN_EXPIRY_SKEW_MS = 60_000;
|
const TOKEN_EXPIRY_SKEW_MS = 60_000;
|
||||||
|
|
||||||
const getOAuthConfigForEnv = ({
|
const getOAuthConfigForEnv = (env: Env) => ({
|
||||||
revenueCatConfig,
|
revenueCatConfig,
|
||||||
env,
|
env,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { AppEnv } from "@autumn/shared";
|
import { AppEnv } from "@autumn/shared";
|
||||||
import type { initRevenuecatCli } from "./initRevenuecatCli.js";
|
import type { initRevenuecatCli } from "./initRevenuecatCli.js";
|
||||||
|
|
||||||
@@ -8,19 +7,21 @@ type RcCli = ReturnType<typeof initRevenuecatCli>;
|
|||||||
* Outbound base URL for our webhook receiver. Dev/staging use NGROK_URL (so RevenueCat
|
* Outbound base URL for our webhook receiver. Dev/staging use NGROK_URL (so RevenueCat
|
||||||
* can reach a local tunnel); production uses BETTER_AUTH_URL.
|
* can reach a local tunnel); production uses BETTER_AUTH_URL.
|
||||||
*/
|
*/
|
||||||
const getServerBaseUrl = (): string | undefined =>
|
const getServerBaseUrl = (env: Partial<Env>): string | undefined =>
|
||||||
runtimeEnv.NODE_ENV !== "production"
|
env.NODE_ENV !== "production"
|
||||||
? runtimeEnv.NGROK_URL
|
? env.NGROK_URL
|
||||||
: runtimeEnv.BETTER_AUTH_URL;
|
: env.BETTER_AUTH_URL;
|
||||||
|
|
||||||
export const getRevenuecatWebhookUrl = ({
|
export const getRevenuecatWebhookUrl = ({
|
||||||
orgId,
|
orgId,
|
||||||
env,
|
env,
|
||||||
|
serverEnv,
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
|
serverEnv: Partial<Env>;
|
||||||
}): string | null => {
|
}): string | null => {
|
||||||
const base = getServerBaseUrl();
|
const base = getServerBaseUrl(serverEnv);
|
||||||
if (!base) return null;
|
if (!base) return null;
|
||||||
// `:env` segment is the AppEnv value ("sandbox"/"live") — revenueCatMiddleware reads it verbatim.
|
// `:env` segment is the AppEnv value ("sandbox"/"live") — revenueCatMiddleware reads it verbatim.
|
||||||
return `${base.replace(/\/$/, "")}/webhooks/revenuecat/${orgId}/${env}`;
|
return `${base.replace(/\/$/, "")}/webhooks/revenuecat/${orgId}/${env}`;
|
||||||
@@ -35,13 +36,15 @@ export const registerRevenuecatWebhook = async ({
|
|||||||
orgId,
|
orgId,
|
||||||
env,
|
env,
|
||||||
secret,
|
secret,
|
||||||
|
serverEnv,
|
||||||
}: {
|
}: {
|
||||||
rcCli: RcCli;
|
rcCli: RcCli;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
secret: string;
|
secret: string;
|
||||||
|
serverEnv: Partial<Env>;
|
||||||
}): Promise<"exists" | "created" | "skipped"> => {
|
}): Promise<"exists" | "created" | "skipped"> => {
|
||||||
const url = getRevenuecatWebhookUrl({ orgId, env });
|
const url = getRevenuecatWebhookUrl({ orgId, env, serverEnv });
|
||||||
if (!url) return "skipped";
|
if (!url) return "skipped";
|
||||||
|
|
||||||
const existing = await rcCli.listWebhookIntegrations();
|
const existing = await rcCli.listWebhookIntegrations();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import {
|
import {
|
||||||
CodeChallengeMethod,
|
CodeChallengeMethod,
|
||||||
generateCodeVerifier,
|
generateCodeVerifier,
|
||||||
@@ -48,22 +47,22 @@ export const findMissingRcScopes = (grantedScopes: string[]): string[] => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRcOAuthClient = () => {
|
const getRcOAuthClient = (env: Env) => {
|
||||||
const clientId = runtimeEnv.REVENUECAT_OAUTH_CLIENT_ID;
|
const clientId = env.REVENUECAT_OAUTH_CLIENT_ID;
|
||||||
const clientSecret = runtimeEnv.REVENUECAT_OAUTH_CLIENT_SECRET;
|
const clientSecret = env.REVENUECAT_OAUTH_CLIENT_SECRET;
|
||||||
|
|
||||||
if (!clientId || !clientSecret) {
|
if (!clientId || !clientSecret) {
|
||||||
throw new Error("RevenueCat OAuth client credentials not configured");
|
throw new Error("RevenueCat OAuth client credentials not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OAuth2Client(clientId, clientSecret, getRcOAuthRedirectUri());
|
return new OAuth2Client(clientId, clientSecret, getRcOAuthRedirectUri(env));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRcOAuthRedirectUri = () => {
|
export const getRcOAuthRedirectUri = (env: Env) => {
|
||||||
let serverUrl = runtimeEnv.BETTER_AUTH_URL;
|
let serverUrl = env.BETTER_AUTH_URL;
|
||||||
|
|
||||||
if (runtimeEnv.NGROK_URL) {
|
if (env.NGROK_URL) {
|
||||||
serverUrl = runtimeEnv.NGROK_URL;
|
serverUrl = env.NGROK_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${(serverUrl ?? "").replace(/\/+$/, "")}/revenuecat/oauth_callback`;
|
return `${(serverUrl ?? "").replace(/\/+$/, "")}/revenuecat/oauth_callback`;
|
||||||
@@ -73,12 +72,14 @@ export const createRcAuthorizationUrl = ({
|
|||||||
state,
|
state,
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
scopes = RC_OAUTH_SCOPES,
|
scopes = RC_OAUTH_SCOPES,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
state: string;
|
state: string;
|
||||||
codeVerifier: string;
|
codeVerifier: string;
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
|
env: Env;
|
||||||
}) => {
|
}) => {
|
||||||
const client = getRcOAuthClient();
|
const client = getRcOAuthClient(env);
|
||||||
return client.createAuthorizationURLWithPKCE(
|
return client.createAuthorizationURLWithPKCE(
|
||||||
RC_AUTHORIZE_URL,
|
RC_AUTHORIZE_URL,
|
||||||
state,
|
state,
|
||||||
@@ -91,11 +92,13 @@ export const createRcAuthorizationUrl = ({
|
|||||||
export const exchangeRcCode = async ({
|
export const exchangeRcCode = async ({
|
||||||
code,
|
code,
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
code: string;
|
code: string;
|
||||||
codeVerifier: string;
|
codeVerifier: string;
|
||||||
|
env: Env;
|
||||||
}) => {
|
}) => {
|
||||||
const client = getRcOAuthClient();
|
const client = getRcOAuthClient(env);
|
||||||
return client.validateAuthorizationCode(RC_TOKEN_URL, code, codeVerifier);
|
return client.validateAuthorizationCode(RC_TOKEN_URL, code, codeVerifier);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,11 +107,13 @@ export const refreshRcTokens = async ({
|
|||||||
// Omit scopes on refresh — re-requesting the full set triggers RC `invalid_scope`.
|
// Omit scopes on refresh — re-requesting the full set triggers RC `invalid_scope`.
|
||||||
// An empty list reuses the originally-granted scopes (OAuth2 §6).
|
// An empty list reuses the originally-granted scopes (OAuth2 §6).
|
||||||
scopes = [],
|
scopes = [],
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
|
env: Env;
|
||||||
}) => {
|
}) => {
|
||||||
const client = getRcOAuthClient();
|
const client = getRcOAuthClient(env);
|
||||||
return client.refreshAccessToken(RC_TOKEN_URL, refreshToken, scopes);
|
return client.refreshAccessToken(RC_TOKEN_URL, refreshToken, scopes);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import * as Sentry from "@sentry/bun";
|
import * as Sentry from "@sentry/bun";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { Stripe } from "stripe";
|
import { Stripe } from "stripe";
|
||||||
@@ -123,7 +122,7 @@ export const handleStripeWebhookEvent = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
runtimeEnv.NODE_ENV === "development" &&
|
env.NODE_ENV === "development" &&
|
||||||
error instanceof Error &&
|
error instanceof Error &&
|
||||||
error.message.includes("No stripe account linked to organization")
|
error.message.includes("No stripe account linked to organization")
|
||||||
) {
|
) {
|
||||||
|
|||||||
2
server/src/external/stripe/stripeCusUtils.ts
vendored
2
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -18,7 +18,7 @@ import { createStripeCustomer } from "@/external/stripe/customers";
|
|||||||
import { CusService } from "@/internal/customers/CusService.js";
|
import { CusService } from "@/internal/customers/CusService.js";
|
||||||
|
|
||||||
import type { TestContext } from "../../../tests/utils/testInitUtils/createTestContext";
|
import type { TestContext } from "../../../tests/utils/testInitUtils/createTestContext";
|
||||||
import { logger } from "../logtail/logtailUtils";
|
import { createLogger } from "../logtail/logtailUtils";
|
||||||
|
|
||||||
const getStripeCus = async ({
|
const getStripeCus = async ({
|
||||||
stripeCli,
|
stripeCli,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { type AppEnv, ErrCode } from "@autumn/shared";
|
import { type AppEnv, ErrCode } from "@autumn/shared";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import RecaseError from "@/utils/errorUtils.js";
|
import RecaseError from "@/utils/errorUtils.js";
|
||||||
@@ -21,7 +20,7 @@ export const createWebhookEndpoint = async (
|
|||||||
) => {
|
) => {
|
||||||
const stripe = new Stripe(apiKey);
|
const stripe = new Stripe(apiKey);
|
||||||
|
|
||||||
const webhookBaseUrl = runtimeEnv.STRIPE_WEBHOOK_URL || runtimeEnv.SERVER_URL;
|
const webhookBaseUrl = env.STRIPE_WEBHOOK_URL || env.SERVER_URL;
|
||||||
|
|
||||||
if (!webhookBaseUrl) {
|
if (!webhookBaseUrl) {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
type Price,
|
type Price,
|
||||||
type ProductOptions,
|
type ProductOptions,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||||
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { getPrimaryRedis } from "@/external/redis/initRedis";
|
import { getPrimaryRedis } from "@/external/redis/initRedis";
|
||||||
import { tryRedisRead, tryRedisWrite } from "@/utils/cacheUtils/cacheUtils";
|
import { tryRedisRead, tryRedisWrite } from "@/utils/cacheUtils/cacheUtils";
|
||||||
|
|
||||||
export const setStripeSubscriptionLock = async ({
|
export const setStripeSubscriptionLock = async ({
|
||||||
stripeSubscriptionId,
|
stripeSubscriptionId,
|
||||||
lockedAtMs,
|
lockedAtMs,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
stripeSubscriptionId: string;
|
stripeSubscriptionId: string;
|
||||||
lockedAtMs: number;
|
lockedAtMs: number;
|
||||||
|
env: Env;
|
||||||
}) => {
|
}) => {
|
||||||
const primaryRedis = getPrimaryRedis();
|
const primaryRedis = getPrimaryRedis();
|
||||||
await tryRedisWrite(
|
await tryRedisWrite(
|
||||||
|
env,
|
||||||
async () =>
|
async () =>
|
||||||
primaryRedis.set(
|
primaryRedis.set(
|
||||||
`sub:${stripeSubscriptionId}`,
|
`sub:${stripeSubscriptionId}`,
|
||||||
JSON.stringify({ lockedAtMs }),
|
JSON.stringify({ lockedAtMs }),
|
||||||
"EX",
|
"EX",
|
||||||
runtimeEnv.NODE_ENV === "production" ? 60 : 3,
|
env.NODE_ENV === "production" ? 60 : 3,
|
||||||
),
|
),
|
||||||
primaryRedis,
|
primaryRedis,
|
||||||
);
|
);
|
||||||
@@ -28,11 +30,13 @@ type StripeSubscriptionLock = {
|
|||||||
|
|
||||||
export const getStripeSubscriptionLock = async ({
|
export const getStripeSubscriptionLock = async ({
|
||||||
stripeSubscriptionId,
|
stripeSubscriptionId,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
stripeSubscriptionId: string;
|
stripeSubscriptionId: string;
|
||||||
|
env: Env;
|
||||||
}): Promise<StripeSubscriptionLock | null> => {
|
}): Promise<StripeSubscriptionLock | null> => {
|
||||||
const primaryRedis = getPrimaryRedis();
|
const primaryRedis = getPrimaryRedis();
|
||||||
return tryRedisRead(async () => {
|
return tryRedisRead(env, async () => {
|
||||||
const value = await primaryRedis.get(`sub:${stripeSubscriptionId}`);
|
const value = await primaryRedis.get(`sub:${stripeSubscriptionId}`);
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
return JSON.parse(value) as StripeSubscriptionLock;
|
return JSON.parse(value) as StripeSubscriptionLock;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { createStripeCli } from "@/external/connect/createStripeCli";
|
import { createStripeCli } from "@/external/connect/createStripeCli";
|
||||||
import { isStripeSubscriptionScheduleInLastPhase } from "@/external/stripe/subscriptionSchedules/utils/classifyStripeSubscriptionScheduleUtils";
|
import { isStripeSubscriptionScheduleInLastPhase } from "@/external/stripe/subscriptionSchedules/utils/classifyStripeSubscriptionScheduleUtils";
|
||||||
import { stripeSubscriptionScheduleToPhaseIndex } from "@/external/stripe/subscriptionSchedules/utils/convertStripeSubscriptionScheduleUtils";
|
import { stripeSubscriptionScheduleToPhaseIndex } from "@/external/stripe/subscriptionSchedules/utils/convertStripeSubscriptionScheduleUtils";
|
||||||
@@ -74,7 +73,7 @@ export const releaseScheduleIfLastPhase = async ({
|
|||||||
return true;
|
return true;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (runtimeEnv.NODE_ENV === "development") {
|
if (env.NODE_ENV === "development") {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[handleSchedulePhaseChanges] failed to release schedule: ${error.message}`,
|
`[handleSchedulePhaseChanges] failed to release schedule: ${error.message}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import {
|
import {
|
||||||
type AppEnv,
|
type AppEnv,
|
||||||
AuthType,
|
AuthType,
|
||||||
@@ -55,8 +54,8 @@ export const stripeConnectSeederMiddleware = async (
|
|||||||
const signature = c.req.header("stripe-signature") || "";
|
const signature = c.req.header("stripe-signature") || "";
|
||||||
|
|
||||||
const skipVerify =
|
const skipVerify =
|
||||||
runtimeEnv.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
|
env.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
|
||||||
runtimeEnv.NODE_ENV !== "production";
|
env.NODE_ENV !== "production";
|
||||||
|
|
||||||
let event: Stripe.Event;
|
let event: Stripe.Event;
|
||||||
if (skipVerify) {
|
if (skipVerify) {
|
||||||
@@ -79,7 +78,7 @@ export const stripeConnectSeederMiddleware = async (
|
|||||||
);
|
);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
if (runtimeEnv.NODE_ENV !== "development") {
|
if (env.NODE_ENV !== "development") {
|
||||||
logger.warn(`Webhook verification error: ${message}`);
|
logger.warn(`Webhook verification error: ${message}`);
|
||||||
}
|
}
|
||||||
return c.json({ error: message }, 400);
|
return c.json({ error: message }, 400);
|
||||||
@@ -115,7 +114,7 @@ export const stripeConnectSeederMiddleware = async (
|
|||||||
return c.json({ error: "Failed to resolve org for Stripe webhook" }, 500);
|
return c.json({ error: "Failed to resolve org for Stripe webhook" }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runtimeEnv.NODE_ENV !== "development") {
|
if (env.NODE_ENV !== "development") {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Account ID ${accountId} not linked to any org, skipping Stripe webhook`,
|
`Account ID ${accountId} not linked to any org, skipping Stripe webhook`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { tryCatch } from "@autumn/shared";
|
import { tryCatch } from "@autumn/shared";
|
||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import { redis } from "@/external/redis/initRedis";
|
import { redis } from "@/external/redis/initRedis";
|
||||||
@@ -17,7 +16,7 @@ export const stripeIdempotencyMiddleware = async (
|
|||||||
c: Context<StripeWebhookHonoEnv>,
|
c: Context<StripeWebhookHonoEnv>,
|
||||||
next: Next,
|
next: Next,
|
||||||
) => {
|
) => {
|
||||||
if (runtimeEnv.NODE_ENV === "development") {
|
if (env.NODE_ENV === "development") {
|
||||||
await next();
|
await next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { type AppEnv, AuthType } from "@autumn/shared";
|
import { type AppEnv, AuthType } from "@autumn/shared";
|
||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
@@ -55,8 +54,8 @@ export const stripeLegacySeederMiddleware = async (
|
|||||||
const signature = c.req.header("stripe-signature") || "";
|
const signature = c.req.header("stripe-signature") || "";
|
||||||
|
|
||||||
const skipVerify =
|
const skipVerify =
|
||||||
runtimeEnv.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
|
env.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
|
||||||
runtimeEnv.NODE_ENV !== "production";
|
env.NODE_ENV !== "production";
|
||||||
|
|
||||||
let event: Stripe.Event;
|
let event: Stripe.Event;
|
||||||
if (skipVerify) {
|
if (skipVerify) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { isSyncableEvent, processStripeSyncEvent } from "@autumn/stripe-sync";
|
import { isSyncableEvent, processStripeSyncEvent } from "@autumn/stripe-sync";
|
||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import { isStripeSyncEnabled } from "@/internal/misc/stripeSync/stripeSyncStore.js";
|
import { isStripeSyncEnabled } from "@/internal/misc/stripeSync/stripeSyncStore.js";
|
||||||
@@ -22,7 +21,7 @@ export const stripeSyncMiddleware = async (
|
|||||||
|
|
||||||
if (!org || !stripeEvent) return;
|
if (!org || !stripeEvent) return;
|
||||||
if (
|
if (
|
||||||
runtimeEnv.NODE_ENV === "production" &&
|
c.env.NODE_ENV === "production" &&
|
||||||
!isStripeSyncEnabled({ orgId: org.id, orgSlug: org.slug })
|
!isStripeSyncEnabled({ orgId: org.id, orgSlug: org.slug })
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface StripeWebhookContext extends AutumnContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type StripeWebhookHonoEnv = {
|
export type StripeWebhookHonoEnv = {
|
||||||
|
Bindings: Env;
|
||||||
Variables: {
|
Variables: {
|
||||||
ctx: StripeWebhookContext;
|
ctx: StripeWebhookContext;
|
||||||
validated: boolean;
|
validated: boolean;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { createClient } from "@supabase/supabase-js";
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
export const createSupabaseClient = () => {
|
export const createSupabaseClient = (env: Env) => {
|
||||||
try {
|
try {
|
||||||
return createClient(
|
return createClient(
|
||||||
runtimeEnv.SUPABASE_URL!,
|
env.SUPABASE_URL!,
|
||||||
runtimeEnv.SUPABASE_SERVICE_KEY!,
|
env.SUPABASE_SERVICE_KEY!,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating Supabase client:", error);
|
console.error("Error creating Supabase client:", error);
|
||||||
|
|||||||
12
server/src/external/supabase/storageUtils.ts
vendored
12
server/src/external/supabase/storageUtils.ts
vendored
@@ -1,13 +1,15 @@
|
|||||||
import { createSupabaseClient } from "@/external/supabase/createSupabaseClient";
|
import { createSupabaseClient } from "@/external/supabase/createSupabaseClient";
|
||||||
|
|
||||||
const readFile = async ({
|
const readFile = async ({
|
||||||
|
env,
|
||||||
bucket = "autumn",
|
bucket = "autumn",
|
||||||
path,
|
path,
|
||||||
}: {
|
}: {
|
||||||
|
env: Env;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
path: string;
|
path: string;
|
||||||
}) => {
|
}) => {
|
||||||
const sb = createSupabaseClient();
|
const sb = createSupabaseClient(env);
|
||||||
const { data, error } = await sb.storage.from(bucket).download(path);
|
const { data, error } = await sb.storage.from(bucket).download(path);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -17,15 +19,17 @@ const readFile = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const uploadFile = async ({
|
const uploadFile = async ({
|
||||||
|
env,
|
||||||
path,
|
path,
|
||||||
file,
|
file,
|
||||||
contentType,
|
contentType,
|
||||||
}: {
|
}: {
|
||||||
|
env: Env;
|
||||||
path: string;
|
path: string;
|
||||||
file: Buffer;
|
file: Buffer;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const sb = createSupabaseClient();
|
const sb = createSupabaseClient(env);
|
||||||
|
|
||||||
const { data, error } = await sb.storage.from("autumn").upload(path, file, {
|
const { data, error } = await sb.storage.from("autumn").upload(path, file, {
|
||||||
upsert: true,
|
upsert: true,
|
||||||
@@ -39,8 +43,8 @@ const uploadFile = async ({
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUploadUrl = async ({ path }: { path: string }) => {
|
export const getUploadUrl = async ({ env, path }: { env: Env; path: string }) => {
|
||||||
const sb = createSupabaseClient();
|
const sb = createSupabaseClient(env);
|
||||||
await sb.storage.from("autumn").remove([path]);
|
await sb.storage.from("autumn").remove([path]);
|
||||||
|
|
||||||
const { data, error } = await sb.storage
|
const { data, error } = await sb.storage
|
||||||
|
|||||||
3
server/src/external/svix/svixHelpers.ts
vendored
3
server/src/external/svix/svixHelpers.ts
vendored
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import type { AppEnv, Organization } from "@autumn/shared";
|
import type { AppEnv, Organization } from "@autumn/shared";
|
||||||
import * as Sentry from "@sentry/bun";
|
import * as Sentry from "@sentry/bun";
|
||||||
import { getSentryTags } from "@/external/sentry/sentryUtils.js";
|
import { getSentryTags } from "@/external/sentry/sentryUtils.js";
|
||||||
@@ -54,7 +53,7 @@ export const sendSvixEvent = async ({
|
|||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
if (!runtimeEnv.SVIX_API_KEY) return;
|
if (!env.SVIX_API_KEY) return;
|
||||||
|
|
||||||
const { org, env } = ctx;
|
const { org, env } = ctx;
|
||||||
|
|
||||||
|
|||||||
13
server/src/external/svix/svixUtils.ts
vendored
13
server/src/external/svix/svixUtils.ts
vendored
@@ -1,27 +1,30 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { AppEnv, type Organization } from "@autumn/shared";
|
import { AppEnv, type Organization } from "@autumn/shared";
|
||||||
import { Svix } from "svix";
|
import { Svix } from "svix";
|
||||||
import { logger } from "../logtail/logtailUtils.js";
|
import { createLogger } from "../logtail/logtailUtils.js";
|
||||||
|
|
||||||
export const createSvixCli = () => {
|
export const createSvixCli = (env: Env) => {
|
||||||
return new Svix(runtimeEnv.SVIX_API_KEY as string);
|
return new Svix(env.SVIX_API_KEY as string);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function safeSvix<T extends (...args: any[]) => any>({
|
export function safeSvix<T extends (...args: any[]) => any>({
|
||||||
fn,
|
fn,
|
||||||
action,
|
action,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
fn: T;
|
fn: T;
|
||||||
action: string;
|
action: string;
|
||||||
|
env: Env;
|
||||||
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
||||||
return async (...args: Parameters<T>) => {
|
return async (...args: Parameters<T>) => {
|
||||||
if (!runtimeEnv.SVIX_API_KEY) {
|
if (!env.SVIX_API_KEY) {
|
||||||
|
const logger = createLogger(env);
|
||||||
logger.warn(`SVIX_API_KEY is not set, skipping ${action}`);
|
logger.warn(`SVIX_API_KEY is not set, skipping ${action}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await fn(...args);
|
return await fn(...args);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const logger = createLogger(env);
|
||||||
logger.error(`Error ${action}: ${error}`);
|
logger.error(`Error ${action}: ${error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
15
server/src/external/tinybird/initClickhouse.ts
vendored
15
server/src/external/tinybird/initClickhouse.ts
vendored
@@ -1,8 +1,11 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { type ClickHouseClient, createClient } from "@clickhouse/client";
|
import { type ClickHouseClient, createClient } from "@clickhouse/client";
|
||||||
|
|
||||||
const TINYBIRD_CLICKHOUSE_URL = runtimeEnv.TINYBIRD_US_EAST_CLICKHOUSE_URL;
|
/** ClickHouse client for raw SQL queries to Tinybird. Null if not configured. */
|
||||||
const TINYBIRD_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN;
|
let clickhouseClient: ClickHouseClient | null = null;
|
||||||
|
|
||||||
|
export const initClickhouse = (env: Env) => {
|
||||||
|
const TINYBIRD_CLICKHOUSE_URL = env.TINYBIRD_US_EAST_CLICKHOUSE_URL;
|
||||||
|
const TINYBIRD_TOKEN = env.TINYBIRD_US_EAST_TOKEN;
|
||||||
|
|
||||||
if (TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN) {
|
if (TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN) {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -10,14 +13,16 @@ if (TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ClickHouse client for raw SQL queries to Tinybird. Null if not configured. */
|
clickhouseClient =
|
||||||
export const clickhouseClient: ClickHouseClient | null =
|
|
||||||
TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN
|
TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN
|
||||||
? createClient({
|
? createClient({
|
||||||
url: TINYBIRD_CLICKHOUSE_URL,
|
url: TINYBIRD_CLICKHOUSE_URL,
|
||||||
password: TINYBIRD_TOKEN,
|
password: TINYBIRD_TOKEN,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { clickhouseClient };
|
||||||
|
|
||||||
/** Get ClickHouse client, throws if not configured. */
|
/** Get ClickHouse client, throws if not configured. */
|
||||||
export const getClickhouseClient = (): ClickHouseClient => {
|
export const getClickhouseClient = (): ClickHouseClient => {
|
||||||
|
|||||||
34
server/src/external/tinybird/initTinybirdV2.ts
vendored
34
server/src/external/tinybird/initTinybirdV2.ts
vendored
@@ -1,23 +1,29 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
import { createTinybirdApi, type TinybirdApi } from "@tinybirdco/sdk";
|
||||||
import { createTinybirdApi } from "@tinybirdco/sdk";
|
|
||||||
|
|
||||||
const TINYBIRD_SECONDARY_API_URL = runtimeEnv.TINYBIRD_API_URL;
|
export interface TinybirdSecondaryEnv {
|
||||||
const TINYBIRD_SECONDARY_TOKEN = runtimeEnv.TINYBIRD_TOKEN;
|
TINYBIRD_API_URL: string;
|
||||||
|
TINYBIRD_TOKEN: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** Secondary Tinybird API client for dual-write safety net during region cutover.
|
/** 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
|
* Reads from the legacy TINYBIRD_API_URL / TINYBIRD_TOKEN env vars (europe-west2
|
||||||
* GCP). Once us-east is stable, delete this file + the dual-write logic in
|
* GCP). Once us-east is stable, delete this file + the dual-write logic in
|
||||||
* sendEvents.ts. */
|
* sendEvents.ts.
|
||||||
export const tinybirdSecondaryApi =
|
* Initialized via configureTinybirdSecondaryApi(). Null until initialized. */
|
||||||
TINYBIRD_SECONDARY_API_URL && TINYBIRD_SECONDARY_TOKEN
|
export let tinybirdSecondaryApi: TinybirdApi | null = null;
|
||||||
? createTinybirdApi({
|
|
||||||
baseUrl: TINYBIRD_SECONDARY_API_URL,
|
|
||||||
token: TINYBIRD_SECONDARY_TOKEN,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (tinybirdSecondaryApi) {
|
/** Configure the secondary Tinybird API client. Must be called during initialization. */
|
||||||
|
export const configureTinybirdSecondaryApi = (env: TinybirdSecondaryEnv): void => {
|
||||||
|
const apiUrl = env.TINYBIRD_API_URL;
|
||||||
|
const token = env.TINYBIRD_TOKEN;
|
||||||
|
|
||||||
|
if (apiUrl && token) {
|
||||||
|
tinybirdSecondaryApi = createTinybirdApi({
|
||||||
|
baseUrl: apiUrl,
|
||||||
|
token,
|
||||||
|
});
|
||||||
console.log(
|
console.log(
|
||||||
`[Tinybird] secondary dual-write configured with URL: ${TINYBIRD_SECONDARY_API_URL}`,
|
`[Tinybird] secondary dual-write configured with URL: ${apiUrl}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import {
|
import {
|
||||||
defineDatasource,
|
defineDatasource,
|
||||||
defineEndpoint,
|
defineEndpoint,
|
||||||
@@ -10,16 +9,32 @@ import {
|
|||||||
t,
|
t,
|
||||||
} from "@tinybirdco/sdk";
|
} from "@tinybirdco/sdk";
|
||||||
|
|
||||||
const TINYBIRD_US_EAST_API_URL = runtimeEnv.TINYBIRD_US_EAST_API_URL;
|
export const getTinybirdApiUrl = (env: Env) => env.TINYBIRD_US_EAST_API_URL;
|
||||||
const TINYBIRD_US_EAST_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN;
|
export const getTinybirdToken = (env: Env) => env.TINYBIRD_US_EAST_TOKEN;
|
||||||
|
|
||||||
const migrationTinybirdConfig =
|
/** Tinybird client for migration item events. Null until initMigrationTinybird(env) is called. */
|
||||||
TINYBIRD_US_EAST_API_URL && TINYBIRD_US_EAST_TOKEN
|
export let migrationTinybird: Tinybird | null = null;
|
||||||
? {
|
|
||||||
baseUrl: TINYBIRD_US_EAST_API_URL,
|
export const initMigrationTinybird = (env: Env) => {
|
||||||
token: TINYBIRD_US_EAST_TOKEN,
|
const apiUrl = env.TINYBIRD_US_EAST_API_URL;
|
||||||
|
const token = env.TINYBIRD_US_EAST_TOKEN;
|
||||||
|
|
||||||
|
if (!apiUrl || !token) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
: null;
|
|
||||||
|
migrationTinybird = new Tinybird({
|
||||||
|
datasources: {
|
||||||
|
itemEvents: migrationItemEventsDatasource,
|
||||||
|
},
|
||||||
|
pipes: {
|
||||||
|
listItemEvents: listMigrationItemEventsEndpoint,
|
||||||
|
},
|
||||||
|
baseUrl: apiUrl,
|
||||||
|
token,
|
||||||
|
devMode: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export type MigrationItemEventStatus = "succeeded" | "skipped" | "failed";
|
export type MigrationItemEventStatus = "succeeded" | "skipped" | "failed";
|
||||||
|
|
||||||
@@ -124,16 +139,3 @@ export const listMigrationItemEventsEndpoint = defineEndpoint(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const migrationTinybird = migrationTinybirdConfig
|
|
||||||
? new Tinybird({
|
|
||||||
datasources: {
|
|
||||||
itemEvents: migrationItemEventsDatasource,
|
|
||||||
},
|
|
||||||
pipes: {
|
|
||||||
listItemEvents: listMigrationItemEventsEndpoint,
|
|
||||||
},
|
|
||||||
...migrationTinybirdConfig,
|
|
||||||
devMode: false,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|||||||
11
server/src/external/tinybird/tinybirdUtils.ts
vendored
11
server/src/external/tinybird/tinybirdUtils.ts
vendored
@@ -1,22 +1,23 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { ErrCode, RecaseError } from "@autumn/shared";
|
import { ErrCode, RecaseError } from "@autumn/shared";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
|
||||||
const TINYBIRD_API_URL = runtimeEnv.TINYBIRD_US_EAST_API_URL;
|
|
||||||
const TINYBIRD_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN;
|
|
||||||
|
|
||||||
export type TinybirdConfig = {
|
export type TinybirdConfig = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tinybirdConfig: TinybirdConfig | null =
|
export let tinybirdConfig: TinybirdConfig | null = null;
|
||||||
|
|
||||||
|
export const initTinybirdConfig = (env: Env) => {
|
||||||
|
|
||||||
|
tinybirdConfig =
|
||||||
TINYBIRD_API_URL && TINYBIRD_TOKEN
|
TINYBIRD_API_URL && TINYBIRD_TOKEN
|
||||||
? {
|
? {
|
||||||
baseUrl: TINYBIRD_API_URL,
|
baseUrl: TINYBIRD_API_URL,
|
||||||
token: TINYBIRD_TOKEN,
|
token: TINYBIRD_TOKEN,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
/** Check if Tinybird is configured. */
|
/** Check if Tinybird is configured. */
|
||||||
export const isTinybirdConfigured = (): boolean => tinybirdConfig !== null;
|
export const isTinybirdConfigured = (): boolean => tinybirdConfig !== null;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { AppEnv, type Organization } from "@autumn/shared";
|
import { AppEnv, type Organization } from "@autumn/shared";
|
||||||
import { createRemoteJWKSet, jwtVerify } from "jose";
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
||||||
import { JWTExpired, JWTInvalid } from "jose/errors";
|
import { JWTExpired, JWTInvalid } from "jose/errors";
|
||||||
@@ -43,7 +42,7 @@ const synthesizeTestClaims = ({
|
|||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
testOptions?: VercelOidcTestOptions;
|
testOptions?: VercelOidcTestOptions;
|
||||||
}): OidcClaims | null => {
|
}): OidcClaims | null => {
|
||||||
if (runtimeEnv.NODE_ENV === "production") return null;
|
if (env.NODE_ENV === "production") return null;
|
||||||
if (testOptions?.allowVercelTestOidc !== true) return null;
|
if (testOptions?.allowVercelTestOidc !== true) return null;
|
||||||
if (!token.startsWith(TEST_OIDC_PREFIX)) return null;
|
if (!token.startsWith(TEST_OIDC_PREFIX)) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
export type VercelSdkTestOptions = {
|
export type VercelSdkTestOptions = {
|
||||||
mockVercelApi?: boolean;
|
mockVercelApi?: boolean;
|
||||||
};
|
};
|
||||||
@@ -6,12 +5,16 @@ export type VercelSdkTestOptions = {
|
|||||||
/**
|
/**
|
||||||
* Only tests opt into the local Vercel SDK mock; dev/manual flows hit Vercel.
|
* Only tests opt into the local Vercel SDK mock; dev/manual flows hit Vercel.
|
||||||
*/
|
*/
|
||||||
export const getVercelSdkServerURL = (
|
export const getVercelSdkServerURL = ({
|
||||||
testOptions?: VercelSdkTestOptions,
|
env,
|
||||||
): string | undefined => {
|
testOptions,
|
||||||
if (runtimeEnv.NODE_ENV === "production") return undefined;
|
}: {
|
||||||
|
env: Env;
|
||||||
|
testOptions?: VercelSdkTestOptions;
|
||||||
|
}): string | undefined => {
|
||||||
|
if (env.NODE_ENV === "production") return undefined;
|
||||||
if (testOptions?.mockVercelApi !== true) return undefined;
|
if (testOptions?.mockVercelApi !== true) return undefined;
|
||||||
const base = runtimeEnv.BETTER_AUTH_URL;
|
const base = env.BETTER_AUTH_URL;
|
||||||
if (!base) return undefined;
|
if (!base) return undefined;
|
||||||
return `${base.replace(/\/$/, "")}/__test/vercel/api`;
|
return `${base.replace(/\/$/, "")}/__test/vercel/api`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { ErrCode, RecaseError } from "@autumn/shared";
|
import { ErrCode, RecaseError } from "@autumn/shared";
|
||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
import { auth } from "@/utils/auth.js";
|
import { createAuth } from "@/utils/auth.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin auth middleware for Hono
|
* Admin auth middleware for Hono
|
||||||
* Validates that the user has an "admin" role
|
* Validates that the user has an "admin" role
|
||||||
*/
|
*/
|
||||||
export const adminAuthMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
export const adminAuthMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
||||||
|
const auth = createAuth(c.env);
|
||||||
const data = await auth.api.getSession({
|
const data = await auth.api.getSession({
|
||||||
headers: c.req.raw.headers,
|
headers: c.req.raw.headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const handleOAuthMiddleware = async ({
|
|||||||
const env = getOAuthEnvironment({ c });
|
const env = getOAuthEnvironment({ c });
|
||||||
const tokenRecord = await getOAuthAccessTokenRecord({
|
const tokenRecord = await getOAuthAccessTokenRecord({
|
||||||
db: ctx.db,
|
db: ctx.db,
|
||||||
|
env: c.env,
|
||||||
accessToken: token,
|
accessToken: token,
|
||||||
resource: c.req.header("x-autumn-oauth-resource") ?? null,
|
resource: c.req.header("x-autumn-oauth-resource") ?? null,
|
||||||
requestedScopes: null,
|
requestedScopes: null,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import { db, dbGeneral } from "@/db/initDrizzle.js";
|
import { db, dbGeneral } from "@/db/initDrizzle.js";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import { resolveRedisV2 } from "@/external/redis/resolveRedisV2.js";
|
import { resolveRedisV2 } from "@/external/redis/resolveRedisV2.js";
|
||||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
import { generateId } from "@/utils/genUtils.js";
|
import { generateId } from "@/utils/genUtils.js";
|
||||||
@@ -41,6 +41,8 @@ const redactSensitiveRequestBody = ({ body }: { body: unknown }): unknown => {
|
|||||||
* Sets up: db, logger, id, timestamp
|
* Sets up: db, logger, id, timestamp
|
||||||
*/
|
*/
|
||||||
export const baseMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
export const baseMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
||||||
|
const env = c.env;
|
||||||
|
const logger = createLogger(env);
|
||||||
// const env = (c.req.header("app_env") as AppEnv) || AppEnv.Sandbox;
|
// const env = (c.req.header("app_env") as AppEnv) || AppEnv.Sandbox;
|
||||||
const id =
|
const id =
|
||||||
c.req.header("rndr-id") ||
|
c.req.header("rndr-id") ||
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type AppEnv, AuthType, ErrCode, RecaseError } from "@autumn/shared";
|
|||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||||
import { auth } from "@/utils/auth.js";
|
import { createAuth } from "@/utils/auth.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Better Auth middleware for dashboard/session authentication
|
* Better Auth middleware for dashboard/session authentication
|
||||||
@@ -19,6 +19,7 @@ export const betterAuthMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
|||||||
const ctx = c.get("ctx");
|
const ctx = c.get("ctx");
|
||||||
|
|
||||||
// Step 1: Get session from Better Auth
|
// Step 1: Get session from Better Auth
|
||||||
|
const auth = createAuth(c.env);
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: c.req.raw.headers,
|
headers: c.req.raw.headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import { rateLimiter } from "hono-rate-limiter";
|
import { rateLimiter } from "hono-rate-limiter";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
import { shouldUseRedis } from "@/external/redis/initRedis.js";
|
import { shouldUseRedis } from "@/external/redis/initRedis.js";
|
||||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
import { createRateLimitRedisStore } from "@/internal/misc/rateLimiter/rateLimitRedisStore.js";
|
import { createRateLimitRedisStore } from "@/internal/misc/rateLimiter/rateLimitRedisStore.js";
|
||||||
@@ -32,6 +32,8 @@ export const createRouterRateLimiter = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return async (c: Context<HonoEnv>, next: Next) => {
|
return async (c: Context<HonoEnv>, next: Next) => {
|
||||||
|
const logger = createLogger(c.env);
|
||||||
|
|
||||||
if (!shouldUseRedis()) return next();
|
if (!shouldUseRedis()) return next();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
import { createLogger } from "@/external/logtail/logtailUtils.js";
|
||||||
|
import type { Logger } from "@/external/logtail/logtailUtils.js";
|
||||||
import { hasRedisConfig, redis } from "@/external/redis/initRedis.js";
|
import { hasRedisConfig, redis } from "@/external/redis/initRedis.js";
|
||||||
import { hasRedisV2Config, redisV2 } from "@/external/redis/initRedisV2.js";
|
import { hasRedisV2Config, redisV2 } from "@/external/redis/initRedisV2.js";
|
||||||
import type { HonoEnv } from "./HonoEnv";
|
import type { HonoEnv } from "./HonoEnv";
|
||||||
@@ -7,6 +8,27 @@ import { evaluateStartupGate } from "./startupGate.js";
|
|||||||
|
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
let startupReady = false;
|
let startupReady = false;
|
||||||
|
let _logger: Logger | null = null;
|
||||||
|
|
||||||
|
const getLogger = (): Logger => {
|
||||||
|
if (!_logger) {
|
||||||
|
throw new Error("handleHealthCheck not initialized — call initHealthCheck(env) first");
|
||||||
|
}
|
||||||
|
return _logger;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the health check module. Must be called before the
|
||||||
|
* health check endpoint is used and before Redis events fire.
|
||||||
|
*/
|
||||||
|
export const initHealthCheck = (env: Env): void => {
|
||||||
|
if (_logger) return;
|
||||||
|
_logger = createLogger(env);
|
||||||
|
|
||||||
|
if (hasRedisConfig) redis.once("ready", tryLatchStartupReady);
|
||||||
|
if (hasRedisV2Config) redisV2.once("ready", tryLatchStartupReady);
|
||||||
|
tryLatchStartupReady();
|
||||||
|
};
|
||||||
|
|
||||||
const tryLatchStartupReady = () => {
|
const tryLatchStartupReady = () => {
|
||||||
if (startupReady) return;
|
if (startupReady) return;
|
||||||
@@ -17,7 +39,7 @@ const tryLatchStartupReady = () => {
|
|||||||
});
|
});
|
||||||
if (!ready) return;
|
if (!ready) return;
|
||||||
startupReady = true;
|
startupReady = true;
|
||||||
logger.info(`[health-check] startup gate latched (${reason})`, {
|
getLogger().info(`[health-check] startup gate latched (${reason})`, {
|
||||||
redis_status: redis.status,
|
redis_status: redis.status,
|
||||||
redis_v2_status: redisV2.status,
|
redis_v2_status: redisV2.status,
|
||||||
has_redis_config: hasRedisConfig,
|
has_redis_config: hasRedisConfig,
|
||||||
@@ -25,11 +47,10 @@ const tryLatchStartupReady = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasRedisConfig) redis.once("ready", tryLatchStartupReady);
|
|
||||||
if (hasRedisV2Config) redisV2.once("ready", tryLatchStartupReady);
|
|
||||||
tryLatchStartupReady();
|
|
||||||
|
|
||||||
export const handleHealthCheck = async (c: Context<HonoEnv>) => {
|
export const handleHealthCheck = async (c: Context<HonoEnv>) => {
|
||||||
|
if (!_logger) {
|
||||||
|
initHealthCheck(c.env);
|
||||||
|
}
|
||||||
if (!startupReady) {
|
if (!startupReady) {
|
||||||
tryLatchStartupReady();
|
tryLatchStartupReady();
|
||||||
if (!startupReady) {
|
if (!startupReady) {
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Sentry + OpenTelemetry must be imported before any application code
|
|
||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
await import("./sentry.js");
|
|
||||||
|
|
||||||
import cluster from "node:cluster";
|
import cluster from "node:cluster";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -13,7 +9,8 @@ import {
|
|||||||
} from "./db/pgHealthMonitor.js";
|
} from "./db/pgHealthMonitor.js";
|
||||||
import { startPgPoolMonitor, stopPgPoolMonitor } from "./db/pgPoolMonitor.js";
|
import { startPgPoolMonitor, stopPgPoolMonitor } from "./db/pgPoolMonitor.js";
|
||||||
import { getRedactedDatabaseUrls } from "./db/redactDatabaseUrl.js";
|
import { getRedactedDatabaseUrls } from "./db/redactDatabaseUrl.js";
|
||||||
import { logger } from "./external/logtail/logtailUtils.js";
|
import { createLogger } from "./external/logtail/logtailUtils.js";
|
||||||
|
import { initDrizzleModules } from "./db/initDrizzle.js";
|
||||||
import {
|
import {
|
||||||
startAllEdgeConfigPolling,
|
startAllEdgeConfigPolling,
|
||||||
stopAllEdgeConfigPolling,
|
stopAllEdgeConfigPolling,
|
||||||
@@ -29,8 +26,6 @@ import "./internal/misc/stripeSync/stripeSyncStore.js";
|
|||||||
import "./internal/misc/redisV2Cache/redisV2CacheStore.js";
|
import "./internal/misc/redisV2Cache/redisV2CacheStore.js";
|
||||||
import "./internal/misc/cacheV2Ramp/cacheV2RampStore.js";
|
import "./internal/misc/cacheV2Ramp/cacheV2RampStore.js";
|
||||||
import "./internal/misc/jobQueues/jobQueueStore.js";
|
import "./internal/misc/jobQueues/jobQueueStore.js";
|
||||||
// Side-effect: configures trigger.dev SDK to use TRIGGER_SERVER_SECRET_KEY.
|
|
||||||
import "./trigger/configureTrigger.js";
|
|
||||||
import { closeStripeSyncEngine } from "@autumn/stripe-sync";
|
import { closeStripeSyncEngine } from "@autumn/stripe-sync";
|
||||||
import {
|
import {
|
||||||
startRedisMonitor,
|
startRedisMonitor,
|
||||||
@@ -45,21 +40,29 @@ import {
|
|||||||
} from "./external/redis/initUtils/redisV2Availability.js";
|
} from "./external/redis/initUtils/redisV2Availability.js";
|
||||||
import { preWarmOrgRedisConnections } from "./external/redis/orgRedisPool.js";
|
import { preWarmOrgRedisConnections } from "./external/redis/orgRedisPool.js";
|
||||||
import { createHonoApp } from "./initHono.js";
|
import { createHonoApp } from "./initHono.js";
|
||||||
import { otelSdk } from "./instrumentation.js";
|
import { initTelemetry, otelSdk } from "./instrumentation.js";
|
||||||
|
import { initSentry } from "./sentry.js";
|
||||||
|
import { configureTrigger } from "./trigger/configureTrigger.js";
|
||||||
import { checkEnvVars } from "./utils/initUtils.js";
|
import { checkEnvVars } from "./utils/initUtils.js";
|
||||||
import { startMemoryMonitor } from "./utils/memoryMonitor.js";
|
import { startMemoryMonitor } from "./utils/memoryMonitor.js";
|
||||||
|
|
||||||
checkEnvVars();
|
|
||||||
|
|
||||||
let shuttingDown = false;
|
let shuttingDown = false;
|
||||||
|
|
||||||
const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
|
const init = async ({
|
||||||
logger.info(getRedactedDatabaseUrls(), "DB URLs");
|
startupStartedAt,
|
||||||
|
env,
|
||||||
|
}: {
|
||||||
|
startupStartedAt: number;
|
||||||
|
env: Env;
|
||||||
|
}) => {
|
||||||
|
const logger = createLogger(env);
|
||||||
|
|
||||||
const app = createHonoApp(runtimeEnv);
|
logger.info(getRedactedDatabaseUrls(env), "DB URLs");
|
||||||
|
|
||||||
initPgHealthMonitor({ client: clientCritical });
|
const app = createHonoApp(env);
|
||||||
startPgPoolMonitor();
|
|
||||||
|
initPgHealthMonitor({ client: clientCritical, env });
|
||||||
|
startPgPoolMonitor(env);
|
||||||
|
|
||||||
void warmupRegionalRedis().catch((error) => {
|
void warmupRegionalRedis().catch((error) => {
|
||||||
logger.warn("[Redis] Warmup failed", { error });
|
logger.warn("[Redis] Warmup failed", { error });
|
||||||
@@ -73,9 +76,7 @@ const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
|
|||||||
startRedisMonitor();
|
startRedisMonitor();
|
||||||
startRedisV2Monitor();
|
startRedisV2Monitor();
|
||||||
|
|
||||||
const PORT = runtimeEnv.SERVER_PORT
|
const PORT = env.SERVER_PORT ? Number.parseInt(env.SERVER_PORT) : 8080;
|
||||||
? Number.parseInt(runtimeEnv.SERVER_PORT)
|
|
||||||
: 8080;
|
|
||||||
|
|
||||||
const requestListener = getRequestListener(app.fetch);
|
const requestListener = getRequestListener(app.fetch);
|
||||||
const server = http.createServer(requestListener);
|
const server = http.createServer(requestListener);
|
||||||
@@ -89,17 +90,29 @@ const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
|
|||||||
console.log(
|
console.log(
|
||||||
`Server running on port ${PORT} (${startupDurationMs}ms startup)`,
|
`Server running on port ${PORT} (${startupDurationMs}ms startup)`,
|
||||||
);
|
);
|
||||||
startMemoryMonitor("server", 60_000);
|
startMemoryMonitor("server", env, 60_000);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (runtimeEnv.NODE_ENV === "development") {
|
export const startNodeServer = async (env: Env) => {
|
||||||
registerFatalErrorHandlers();
|
const logger = createLogger(env);
|
||||||
await init({ startupStartedAt: Date.now() });
|
|
||||||
registerShutdownHandlers();
|
initDrizzleModules(env);
|
||||||
} else {
|
initSentry(env);
|
||||||
|
initTelemetry(env);
|
||||||
|
configureTrigger(env);
|
||||||
|
checkEnvVars(env);
|
||||||
|
|
||||||
|
if (env.NODE_ENV === "development") {
|
||||||
|
registerFatalErrorHandlers(logger);
|
||||||
|
await init({ startupStartedAt: Date.now(), env });
|
||||||
|
registerShutdownHandlers(logger);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const numCPUs = os.cpus().length;
|
const numCPUs = os.cpus().length;
|
||||||
|
|
||||||
if (cluster.isPrimary) {
|
if (cluster.isPrimary) {
|
||||||
@@ -123,15 +136,15 @@ if (runtimeEnv.NODE_ENV === "development") {
|
|||||||
cluster.fork();
|
cluster.fork();
|
||||||
});
|
});
|
||||||
|
|
||||||
registerShutdownHandlers();
|
registerShutdownHandlers(logger);
|
||||||
} else {
|
} else {
|
||||||
registerFatalErrorHandlers();
|
registerFatalErrorHandlers(logger);
|
||||||
await init({ startupStartedAt: Date.now() });
|
await init({ startupStartedAt: Date.now(), env });
|
||||||
registerShutdownHandlers();
|
registerShutdownHandlers(logger);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function registerFatalErrorHandlers() {
|
function registerFatalErrorHandlers(logger: ReturnType<typeof createLogger>) {
|
||||||
const exitAfterLog = () => setTimeout(() => process.exit(1), 100);
|
const exitAfterLog = () => setTimeout(() => process.exit(1), 100);
|
||||||
const logFatal = (event: string, error: unknown) => {
|
const logFatal = (event: string, error: unknown) => {
|
||||||
logger.error(event, {
|
logger.error(event, {
|
||||||
@@ -152,13 +165,13 @@ function registerFatalErrorHandlers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerShutdownHandlers() {
|
function registerShutdownHandlers(logger: ReturnType<typeof createLogger>) {
|
||||||
process.on("SIGTERM", gracefulShutdown);
|
process.on("SIGTERM", () => gracefulShutdown(logger));
|
||||||
process.on("SIGINT", gracefulShutdown);
|
process.on("SIGINT", () => gracefulShutdown(logger));
|
||||||
// Do NOT use process.on("exit", ...) for async cleanup!
|
// Do NOT use process.on("exit", ...) for async cleanup!
|
||||||
}
|
}
|
||||||
|
|
||||||
async function gracefulShutdown() {
|
async function gracefulShutdown(logger: ReturnType<typeof createLogger>) {
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
console.log("Shutting down worker, flushing telemetry and closing DB...");
|
console.log("Shutting down worker, flushing telemetry and closing DB...");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { apiRouter } from "./routers/apiRouter.js";
|
|||||||
import { createChatProxyRouter } from "./routers/chatProxyRouter.js";
|
import { createChatProxyRouter } from "./routers/chatProxyRouter.js";
|
||||||
import { createInternalRouter } from "./routers/internalRouter.js";
|
import { createInternalRouter } from "./routers/internalRouter.js";
|
||||||
import { publicRouter } from "./routers/publicRouter.js";
|
import { publicRouter } from "./routers/publicRouter.js";
|
||||||
import { auth } from "./utils/auth.js";
|
import { createAuth } from "./utils/auth.js";
|
||||||
import { isAllowedOrigin } from "./utils/corsOrigins.js";
|
import { isAllowedOrigin } from "./utils/corsOrigins.js";
|
||||||
|
|
||||||
const ALLOWED_HEADERS = [
|
const ALLOWED_HEADERS = [
|
||||||
@@ -50,6 +50,7 @@ const ALLOWED_HEADERS = [
|
|||||||
|
|
||||||
export const createHonoApp = (env: Env) => {
|
export const createHonoApp = (env: Env) => {
|
||||||
const app = new Hono<HonoEnv>();
|
const app = new Hono<HonoEnv>();
|
||||||
|
const auth = createAuth(env);
|
||||||
|
|
||||||
app.route("", createChatProxyRouter(env));
|
app.route("", createChatProxyRouter(env));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { DiagConsoleLogger, DiagLogLevel, diag } from "@opentelemetry/api";
|
import { DiagConsoleLogger, DiagLogLevel, diag } from "@opentelemetry/api";
|
||||||
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
||||||
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||||
@@ -14,14 +13,16 @@ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);
|
|||||||
|
|
||||||
let sdk: NodeSDK | null = null;
|
let sdk: NodeSDK | null = null;
|
||||||
|
|
||||||
if (runtimeEnv.AXIOM_TOKEN) {
|
export const initTelemetry = (env: Env) => {
|
||||||
|
if (!env.AXIOM_TOKEN) return sdk;
|
||||||
|
|
||||||
// NodeSDK reads OTEL_SERVICE_NAME to set the service resource attribute
|
// NodeSDK reads OTEL_SERVICE_NAME to set the service resource attribute
|
||||||
runtimeEnv.OTEL_SERVICE_NAME = "autumn-server";
|
env.OTEL_SERVICE_NAME = "autumn-server";
|
||||||
|
|
||||||
const traceExporter = new OTLPTraceExporter({
|
const traceExporter = new OTLPTraceExporter({
|
||||||
url: "https://api.axiom.co/v1/traces",
|
url: "https://api.axiom.co/v1/traces",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${runtimeEnv.AXIOM_TOKEN}`,
|
Authorization: `Bearer ${env.AXIOM_TOKEN}`,
|
||||||
"X-Axiom-Dataset": "otel",
|
"X-Axiom-Dataset": "otel",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -30,18 +31,18 @@ if (runtimeEnv.AXIOM_TOKEN) {
|
|||||||
// auto-add a BatchSpanProcessor for `traceExporter` when `spanProcessors`
|
// auto-add a BatchSpanProcessor for `traceExporter` when `spanProcessors`
|
||||||
// is set. We must wire the exporter processor explicitly.
|
// is set. We must wire the exporter processor explicitly.
|
||||||
// Dev: short 1s flush for fast feedback. Prod: default 5s for throughput.
|
// Dev: short 1s flush for fast feedback. Prod: default 5s for throughput.
|
||||||
const isDev = runtimeEnv.NODE_ENV !== "production";
|
const isDev = env.NODE_ENV !== "production";
|
||||||
const exportProcessor = new BatchSpanProcessor(traceExporter, {
|
const exportProcessor = new BatchSpanProcessor(traceExporter, {
|
||||||
scheduledDelayMillis: isDev ? 1000 : 5000,
|
scheduledDelayMillis: isDev ? 1000 : 5000,
|
||||||
});
|
});
|
||||||
const filteredExportProcessor = new FilteringSpanProcessor(exportProcessor);
|
const filteredExportProcessor = new FilteringSpanProcessor(exportProcessor);
|
||||||
const metricReader = runtimeEnv.AXIOM_METRICS_DATASET
|
const metricReader = env.AXIOM_METRICS_DATASET
|
||||||
? new PeriodicExportingMetricReader({
|
? new PeriodicExportingMetricReader({
|
||||||
exporter: new OTLPMetricExporter({
|
exporter: new OTLPMetricExporter({
|
||||||
url: "https://api.axiom.co/v1/metrics",
|
url: "https://api.axiom.co/v1/metrics",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${runtimeEnv.AXIOM_TOKEN}`,
|
Authorization: `Bearer ${env.AXIOM_TOKEN}`,
|
||||||
"x-axiom-metrics-dataset": runtimeEnv.AXIOM_METRICS_DATASET,
|
"x-axiom-metrics-dataset": env.AXIOM_METRICS_DATASET,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
exportIntervalMillis: 60_000,
|
exportIntervalMillis: 60_000,
|
||||||
@@ -68,6 +69,8 @@ if (runtimeEnv.AXIOM_TOKEN) {
|
|||||||
};
|
};
|
||||||
process.once("SIGTERM", shutdown);
|
process.once("SIGTERM", shutdown);
|
||||||
process.once("SIGINT", shutdown);
|
process.once("SIGINT", shutdown);
|
||||||
}
|
|
||||||
|
return sdk;
|
||||||
|
};
|
||||||
|
|
||||||
export { sdk as otelSdk };
|
export { sdk as otelSdk };
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import crypto, { randomUUID } from "node:crypto";
|
import crypto, { randomUUID } from "node:crypto";
|
||||||
import { stripOAuthTokenPrefix } from "@autumn/auth";
|
import { stripOAuthTokenPrefix } from "@autumn/auth";
|
||||||
import {
|
import {
|
||||||
@@ -48,9 +47,15 @@ const findTargetOrg = ({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getSlackAdminInstallation = async ({ db }: { db: DrizzleCli }) =>
|
const getSlackAdminInstallation = async ({
|
||||||
|
db,
|
||||||
|
env,
|
||||||
|
}: {
|
||||||
|
db: DrizzleCli;
|
||||||
|
env: Env;
|
||||||
|
}) =>
|
||||||
db.query.chatInstallations.findFirst({
|
db.query.chatInstallations.findFirst({
|
||||||
where: eq(chatInstallations.provider, getSlackAdminProvider()),
|
where: eq(chatInstallations.provider, getSlackAdminProvider({ env })),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getSlackAdminOAuthCredentials = async ({
|
const getSlackAdminOAuthCredentials = async ({
|
||||||
@@ -80,10 +85,16 @@ const getOrgSummary = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptChatCredentialToken = ({ token }: { token: string }) => {
|
const decryptChatCredentialToken = ({
|
||||||
|
env,
|
||||||
|
token,
|
||||||
|
}: {
|
||||||
|
env: Env;
|
||||||
|
token: string;
|
||||||
|
}) => {
|
||||||
const key = crypto
|
const key = crypto
|
||||||
.createHash("sha256")
|
.createHash("sha256")
|
||||||
.update(runtimeEnv.ENCRYPTION_PASSWORD ?? "")
|
.update(env.ENCRYPTION_PASSWORD ?? "")
|
||||||
.digest();
|
.digest();
|
||||||
const buffer = Buffer.from(token, "base64");
|
const buffer = Buffer.from(token, "base64");
|
||||||
if (buffer[0] !== 1) throw new Error("Unsupported encrypted payload");
|
if (buffer[0] !== 1) throw new Error("Unsupported encrypted payload");
|
||||||
@@ -112,9 +123,11 @@ const getStoredOAuthTokenValues = async ({
|
|||||||
|
|
||||||
const revokeSlackAdminOAuthArtifacts = async ({
|
const revokeSlackAdminOAuthArtifacts = async ({
|
||||||
db,
|
db,
|
||||||
|
env,
|
||||||
credentials,
|
credentials,
|
||||||
}: {
|
}: {
|
||||||
db: Pick<DrizzleCli, "delete" | "select">;
|
db: Pick<DrizzleCli, "delete" | "select">;
|
||||||
|
env: Env;
|
||||||
credentials: ChatOAuthCredential[];
|
credentials: ChatOAuthCredential[];
|
||||||
}) => {
|
}) => {
|
||||||
const consentIds = [
|
const consentIds = [
|
||||||
@@ -131,13 +144,17 @@ const revokeSlackAdminOAuthArtifacts = async ({
|
|||||||
for (const credential of credentials) {
|
for (const credential of credentials) {
|
||||||
accessTokenValues.push(
|
accessTokenValues.push(
|
||||||
...(await getStoredOAuthTokenValues({
|
...(await getStoredOAuthTokenValues({
|
||||||
token: decryptChatCredentialToken({ token: credential.access_token }),
|
token: decryptChatCredentialToken({
|
||||||
|
env,
|
||||||
|
token: credential.access_token,
|
||||||
|
}),
|
||||||
stripPrefix: true,
|
stripPrefix: true,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
refreshTokenValues.push(
|
refreshTokenValues.push(
|
||||||
...(await getStoredOAuthTokenValues({
|
...(await getStoredOAuthTokenValues({
|
||||||
token: decryptChatCredentialToken({
|
token: decryptChatCredentialToken({
|
||||||
|
env,
|
||||||
token: credential.refresh_token,
|
token: credential.refresh_token,
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
@@ -186,8 +203,8 @@ export const handleCreateSlackAdminInstall = createRoute({
|
|||||||
handler: async (c) => {
|
handler: async (c) => {
|
||||||
const ctx = c.get("ctx");
|
const ctx = c.get("ctx");
|
||||||
const state = createChatInstallState({
|
const state = createChatInstallState({
|
||||||
secret: getChatStateSecret(),
|
secret: getChatStateSecret(c.env),
|
||||||
provider: getSlackAdminProvider(),
|
provider: getSlackAdminProvider({ env: c.env }),
|
||||||
orgId: ctx.org.id,
|
orgId: ctx.org.id,
|
||||||
userId: ctx.userId ?? "",
|
userId: ctx.userId ?? "",
|
||||||
env: ctx.env,
|
env: ctx.env,
|
||||||
@@ -195,7 +212,7 @@ export const handleCreateSlackAdminInstall = createRoute({
|
|||||||
nonce: randomUUID(),
|
nonce: randomUUID(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json({ url: createSlackInstallUrl(state) });
|
return c.json({ url: createSlackInstallUrl(c.env, state) });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,7 +220,7 @@ export const handleGetSlackAdminInstall = createRoute({
|
|||||||
scopes: [Scopes.Superuser],
|
scopes: [Scopes.Superuser],
|
||||||
handler: async (c) => {
|
handler: async (c) => {
|
||||||
const { db } = c.get("ctx");
|
const { db } = c.get("ctx");
|
||||||
const installation = await getSlackAdminInstallation({ db });
|
const installation = await getSlackAdminInstallation({ db, env: c.env });
|
||||||
const targetOrg = installation
|
const targetOrg = installation
|
||||||
? await getOrgSummary({ db, orgId: installation.org_id })
|
? await getOrgSummary({ db, orgId: installation.org_id })
|
||||||
: null;
|
: null;
|
||||||
@@ -248,7 +265,7 @@ export const handleUpdateSlackAdminTarget = createRoute({
|
|||||||
const ctx = c.get("ctx");
|
const ctx = c.get("ctx");
|
||||||
const { db } = ctx;
|
const { db } = ctx;
|
||||||
const { org_id: orgIdOrSlug, env } = c.req.valid("json");
|
const { org_id: orgIdOrSlug, env } = c.req.valid("json");
|
||||||
const installation = await getSlackAdminInstallation({ db });
|
const installation = await getSlackAdminInstallation({ db, env: c.env });
|
||||||
if (!installation) {
|
if (!installation) {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
message: "Slack admin bot is not installed",
|
message: "Slack admin bot is not installed",
|
||||||
@@ -285,6 +302,7 @@ export const handleUpdateSlackAdminTarget = createRoute({
|
|||||||
|
|
||||||
await revokeSlackAdminOAuthArtifacts({
|
await revokeSlackAdminOAuthArtifacts({
|
||||||
db: tx,
|
db: tx,
|
||||||
|
env: c.env,
|
||||||
credentials: oauthCredentials,
|
credentials: oauthCredentials,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,7 +329,7 @@ export const handleDeleteSlackAdminInstall = createRoute({
|
|||||||
scopes: [Scopes.Superuser],
|
scopes: [Scopes.Superuser],
|
||||||
handler: async (c) => {
|
handler: async (c) => {
|
||||||
const { db } = c.get("ctx");
|
const { db } = c.get("ctx");
|
||||||
const installation = await getSlackAdminInstallation({ db });
|
const installation = await getSlackAdminInstallation({ db, env: c.env });
|
||||||
if (!installation) return c.json({ success: true });
|
if (!installation) return c.json({ success: true });
|
||||||
const oauthCredentials = await getSlackAdminOAuthCredentials({
|
const oauthCredentials = await getSlackAdminOAuthCredentials({
|
||||||
db,
|
db,
|
||||||
@@ -321,6 +339,7 @@ export const handleDeleteSlackAdminInstall = createRoute({
|
|||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
await revokeSlackAdminOAuthArtifacts({
|
await revokeSlackAdminOAuthArtifacts({
|
||||||
db: tx,
|
db: tx,
|
||||||
|
env: c.env,
|
||||||
credentials: oauthCredentials,
|
credentials: oauthCredentials,
|
||||||
});
|
});
|
||||||
await tx
|
await tx
|
||||||
@@ -328,7 +347,10 @@ export const handleDeleteSlackAdminInstall = createRoute({
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(chatInstallations.id, installation.id),
|
eq(chatInstallations.id, installation.id),
|
||||||
eq(chatInstallations.provider, getSlackAdminProvider()),
|
eq(
|
||||||
|
chatInstallations.provider,
|
||||||
|
getSlackAdminProvider({ env: c.env }),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { ErrCode, RecaseError, Scopes } from "@autumn/shared";
|
import { ErrCode, RecaseError, Scopes } from "@autumn/shared";
|
||||||
import { registerMcpOAuthClient } from "@/internal/auth/actions/index.js";
|
import { registerMcpOAuthClient } from "@/internal/auth/actions/index.js";
|
||||||
import { createRoute } from "../../honoMiddlewares/routeHandler";
|
import { createRoute } from "../../honoMiddlewares/routeHandler";
|
||||||
|
|
||||||
const getClientUrl = () =>
|
const getClientUrl = (env: Env) =>
|
||||||
(runtimeEnv.CLIENT_URL || "http://localhost:3000").replace(/\/+$/, "");
|
(env.CLIENT_URL || "http://localhost:3000").replace(/\/+$/, "");
|
||||||
|
|
||||||
const getSlackMcpRedirectUris = () => {
|
const getSlackMcpRedirectUris = (env: Env) => {
|
||||||
const clientUrl = getClientUrl();
|
const clientUrl = getClientUrl(env);
|
||||||
return [
|
return [
|
||||||
`${clientUrl}/admin/oauth/slack-mcp/callback`,
|
`${clientUrl}/admin/oauth/slack-mcp/callback`,
|
||||||
`${clientUrl}/sandbox/admin/oauth/slack-mcp/callback`,
|
`${clientUrl}/sandbox/admin/oauth/slack-mcp/callback`,
|
||||||
@@ -21,7 +20,7 @@ export const handleUpsertSlackMcpOAuthClient = createRoute({
|
|||||||
const result = await registerMcpOAuthClient({
|
const result = await registerMcpOAuthClient({
|
||||||
db,
|
db,
|
||||||
clientName: "Slack MCP",
|
clientName: "Slack MCP",
|
||||||
redirectUris: getSlackMcpRedirectUris(),
|
redirectUris: getSlackMcpRedirectUris(c.env),
|
||||||
scope: undefined,
|
scope: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { ErrCode, type EventInsert, events, RecaseError } from "@autumn/shared";
|
import { ErrCode, type EventInsert, events, RecaseError } from "@autumn/shared";
|
||||||
import { and, desc, eq } from "drizzle-orm";
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
@@ -10,12 +9,14 @@ export class EventService {
|
|||||||
static async insert({
|
static async insert({
|
||||||
db,
|
db,
|
||||||
event,
|
event,
|
||||||
|
env,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
event: EventInsert | EventInsert[];
|
event: EventInsert | EventInsert[];
|
||||||
logger?: Logger;
|
logger?: Logger;
|
||||||
|
env: Env;
|
||||||
}) {
|
}) {
|
||||||
if (runtimeEnv.NODE_ENV !== "development") return;
|
if (env.NODE_ENV !== "development") return;
|
||||||
try {
|
try {
|
||||||
const results = await db
|
const results = await db
|
||||||
.insert(events)
|
.insert(events)
|
||||||
@@ -42,14 +43,16 @@ export class EventService {
|
|||||||
internalCustomerId,
|
internalCustomerId,
|
||||||
env,
|
env,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
|
workerEnv,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
internalCustomerId: string;
|
internalCustomerId: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
env: string;
|
env: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
workerEnv: Env;
|
||||||
}) {
|
}) {
|
||||||
if (runtimeEnv.NODE_ENV === "production") return [];
|
if (workerEnv.NODE_ENV === "production") return [];
|
||||||
const results = await db
|
const results = await db
|
||||||
.select({
|
.select({
|
||||||
id: events.id,
|
id: events.id,
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { ErrCode, member, organizations, RecaseError } from "@autumn/shared";
|
|||||||
import { asc, eq } from "drizzle-orm";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { db } from "@/db/initDrizzle.js";
|
import { db } from "@/db/initDrizzle.js";
|
||||||
import { auth } from "@/utils/auth.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
|
import { createAuth } from "@/utils/auth.js";
|
||||||
|
|
||||||
const AUTH_ORGANIZATION_LIST_LIMIT = 1000;
|
const AUTH_ORGANIZATION_LIST_LIMIT = 1000;
|
||||||
|
|
||||||
export const handleListAuthOrganizations = async (c: Context) => {
|
export const handleListAuthOrganizations = async (c: Context<HonoEnv>) => {
|
||||||
|
const auth = createAuth(c.env);
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: c.req.raw.headers,
|
headers: c.req.raw.headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
import { oauthClientRepo } from "../repos/index.js";
|
import { oauthClientRepo } from "../repos/index.js";
|
||||||
|
|
||||||
const ATMN_OAUTH_CLIENT_NAMES = new Set(["atmn", "autumn cli"]);
|
const ATMN_OAUTH_CLIENT_NAMES = new Set(["atmn", "autumn cli"]);
|
||||||
|
|
||||||
const configuredAtmnClientIds = () =>
|
const configuredAtmnClientIds = (env: Env) =>
|
||||||
new Set(
|
new Set(
|
||||||
(runtimeEnv.ATMN_OAUTH_CLIENT_IDS ?? "")
|
(env.ATMN_OAUTH_CLIENT_IDS ?? "")
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((id) => id.trim())
|
.map((id) => id.trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
@@ -36,15 +35,17 @@ const metadataMarksAtmn = (metadata: unknown) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isAtmnOAuthClientRecord = ({
|
export const isAtmnOAuthClientRecord = ({
|
||||||
|
env,
|
||||||
clientId,
|
clientId,
|
||||||
name,
|
name,
|
||||||
metadata,
|
metadata,
|
||||||
}: {
|
}: {
|
||||||
|
env: Env;
|
||||||
clientId: string | null | undefined;
|
clientId: string | null | undefined;
|
||||||
name: string | null | undefined;
|
name: string | null | undefined;
|
||||||
metadata?: unknown;
|
metadata?: unknown;
|
||||||
}) => {
|
}) => {
|
||||||
if (clientId && configuredAtmnClientIds().has(clientId)) return true;
|
if (clientId && configuredAtmnClientIds(env).has(clientId)) return true;
|
||||||
if (metadataMarksAtmn(metadata)) return true;
|
if (metadataMarksAtmn(metadata)) return true;
|
||||||
|
|
||||||
const normalizedName = name?.trim().toLowerCase();
|
const normalizedName = name?.trim().toLowerCase();
|
||||||
@@ -53,12 +54,17 @@ export const isAtmnOAuthClientRecord = ({
|
|||||||
|
|
||||||
export const isAtmnOAuthClientId = async ({
|
export const isAtmnOAuthClientId = async ({
|
||||||
db,
|
db,
|
||||||
|
env,
|
||||||
clientId,
|
clientId,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
|
env: Env;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const client = await oauthClientRepo.getByClientId({ db, clientId });
|
const client = await oauthClientRepo.getByClientId({ db, clientId });
|
||||||
|
|
||||||
return isAtmnOAuthClientRecord(client ?? { clientId, name: null });
|
return isAtmnOAuthClientRecord({
|
||||||
|
env,
|
||||||
|
...(client ?? { clientId, name: null }),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { db } from "@/db/initDrizzle.js";
|
import { db } from "@/db/initDrizzle.js";
|
||||||
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
import { oauthClientRepo } from "../repos/index.js";
|
import { oauthClientRepo } from "../repos/index.js";
|
||||||
import { isAtmnOAuthClientRecord } from "./atmnOAuthClients.js";
|
import { isAtmnOAuthClientRecord } from "./atmnOAuthClients.js";
|
||||||
import {
|
import {
|
||||||
@@ -7,7 +8,7 @@ import {
|
|||||||
isInternalMcpOAuthClientRecord,
|
isInternalMcpOAuthClientRecord,
|
||||||
} from "./internalMcpOAuthClients.js";
|
} from "./internalMcpOAuthClients.js";
|
||||||
|
|
||||||
export const handleGetOAuthClient = async (c: Context) => {
|
export const handleGetOAuthClient = async (c: Context<HonoEnv>) => {
|
||||||
const clientId = c.req.param("client_id");
|
const clientId = c.req.param("client_id");
|
||||||
const redirectUri = c.req.query("redirect_uri");
|
const redirectUri = c.req.query("redirect_uri");
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
@@ -20,7 +21,10 @@ export const handleGetOAuthClient = async (c: Context) => {
|
|||||||
return c.json({ error: "Client not found" }, 404);
|
return c.json({ error: "Client not found" }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInternalMcp = isInternalMcpOAuthClientRecord(client);
|
const isInternalMcp = isInternalMcpOAuthClientRecord({
|
||||||
|
env: c.env,
|
||||||
|
...client,
|
||||||
|
});
|
||||||
const internalMcpName = isInternalMcp
|
const internalMcpName = isInternalMcp
|
||||||
? getInternalMcpDisplayName({
|
? getInternalMcpDisplayName({
|
||||||
metadata: client.metadata,
|
metadata: client.metadata,
|
||||||
@@ -31,7 +35,7 @@ export const handleGetOAuthClient = async (c: Context) => {
|
|||||||
return c.json({
|
return c.json({
|
||||||
client_id: client.clientId,
|
client_id: client.clientId,
|
||||||
name: internalMcpName || client.name || "Unknown Application",
|
name: internalMcpName || client.name || "Unknown Application",
|
||||||
is_atmn: isAtmnOAuthClientRecord(client),
|
is_atmn: isAtmnOAuthClientRecord({ env: c.env, ...client }),
|
||||||
is_internal_mcp: isInternalMcp,
|
is_internal_mcp: isInternalMcp,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { AppEnv, RecaseError } from "@autumn/shared";
|
import { AppEnv, RecaseError } from "@autumn/shared";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { db } from "@/db/initDrizzle.js";
|
import { db } from "@/db/initDrizzle.js";
|
||||||
import { auth } from "@/utils/auth.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
|
import { createAuth } from "@/utils/auth.js";
|
||||||
import { oauthConsentRepo } from "../repos/index.js";
|
import { oauthConsentRepo } from "../repos/index.js";
|
||||||
import { isAtmnOAuthClientId } from "./atmnOAuthClients.js";
|
import { isAtmnOAuthClientId } from "./atmnOAuthClients.js";
|
||||||
import { getOAuthConsentScopeGrant } from "./oauthConsentScopes.js";
|
import { getOAuthConsentScopeGrant } from "./oauthConsentScopes.js";
|
||||||
@@ -120,7 +121,8 @@ const jsonOAuthError = ({ error }: { error: RecaseError }) =>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const handleOAuthConsentWithEnv = async (c: Context) => {
|
export const handleOAuthConsentWithEnv = async (c: Context<HonoEnv>) => {
|
||||||
|
const auth = createAuth(c.env);
|
||||||
const { contentType, fields } = await parseRequestFields(c.req.raw.clone());
|
const { contentType, fields } = await parseRequestFields(c.req.raw.clone());
|
||||||
const clientId = getClientIdFromFields(fields);
|
const clientId = getClientIdFromFields(fields);
|
||||||
const redirectUri = getRedirectUriFromFields(fields);
|
const redirectUri = getRedirectUriFromFields(fields);
|
||||||
@@ -165,7 +167,11 @@ export const handleOAuthConsentWithEnv = async (c: Context) => {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientId || !env || (await isAtmnOAuthClientId({ db, clientId }))) {
|
if (
|
||||||
|
!clientId ||
|
||||||
|
!env ||
|
||||||
|
(await isAtmnOAuthClientId({ db, env: c.env, clientId }))
|
||||||
|
) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
import { ErrCode, RecaseError } from "@autumn/shared";
|
import { ErrCode, RecaseError } from "@autumn/shared";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { db } from "@/db/initDrizzle.js";
|
import { db } from "@/db/initDrizzle.js";
|
||||||
import { auth } from "@/utils/auth.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
|
import { createAuth } from "@/utils/auth.js";
|
||||||
import { oauthAccessTokenRepo, oauthRefreshTokenRepo } from "../repos/index.js";
|
import { oauthAccessTokenRepo, oauthRefreshTokenRepo } from "../repos/index.js";
|
||||||
import { isMcpOAuthClient } from "./mcpOAuthScopes.js";
|
import { isMcpOAuthClient } from "./mcpOAuthScopes.js";
|
||||||
import {
|
import {
|
||||||
@@ -108,7 +109,8 @@ const jsonTokenResponse = ({
|
|||||||
headers: tokenResponseHeaders(response),
|
headers: tokenResponseHeaders(response),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const handleOAuthTokenWithApiKey = async (c: Context) => {
|
export const handleOAuthTokenWithApiKey = async (c: Context<HonoEnv>) => {
|
||||||
|
const auth = createAuth(c.env);
|
||||||
const resource = await getResourceFromOAuthTokenRequest(c.req.raw.clone());
|
const resource = await getResourceFromOAuthTokenRequest(c.req.raw.clone());
|
||||||
const response = await auth.handler(c.req.raw);
|
const response = await auth.handler(c.req.raw);
|
||||||
if (!response.ok) return response;
|
if (!response.ok) return response;
|
||||||
@@ -132,6 +134,7 @@ export const handleOAuthTokenWithApiKey = async (c: Context) => {
|
|||||||
try {
|
try {
|
||||||
const tokenRecord = await getOAuthAccessTokenRecord({
|
const tokenRecord = await getOAuthAccessTokenRecord({
|
||||||
db,
|
db,
|
||||||
|
env: c.env,
|
||||||
accessToken,
|
accessToken,
|
||||||
resource,
|
resource,
|
||||||
requestedScopes,
|
requestedScopes,
|
||||||
@@ -185,6 +188,7 @@ export const handleOAuthTokenWithApiKey = async (c: Context) => {
|
|||||||
}
|
}
|
||||||
apiKeyResult = await getExternalOAuthApiKeyForToken({
|
apiKeyResult = await getExternalOAuthApiKeyForToken({
|
||||||
db,
|
db,
|
||||||
|
env: c.env,
|
||||||
tokenRecord,
|
tokenRecord,
|
||||||
requestedScopes,
|
requestedScopes,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { MCP_CLIENT_KIND } from "@autumn/auth/oauth";
|
import { MCP_CLIENT_KIND } from "@autumn/auth/oauth";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { type DrizzleCli, db } from "@/db/initDrizzle.js";
|
import { type DrizzleCli, db } from "@/db/initDrizzle.js";
|
||||||
import { auth } from "@/utils/auth.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
|
import { createAuth } from "@/utils/auth.js";
|
||||||
import { oauthClientRepo } from "../repos/index.js";
|
import { oauthClientRepo } from "../repos/index.js";
|
||||||
|
|
||||||
const INTERNAL_MCP_CLIENT_ID = runtimeEnv.INTERNAL_MCP_OAUTH_CLIENT_ID;
|
|
||||||
const INTERNAL_MCP_CLIENT_NAME = "Autumn internal-mcp";
|
const INTERNAL_MCP_CLIENT_NAME = "Autumn internal-mcp";
|
||||||
const INTERNAL_MCP_CLIENT_NAME_NORMALIZED =
|
const INTERNAL_MCP_CLIENT_NAME_NORMALIZED =
|
||||||
INTERNAL_MCP_CLIENT_NAME.toLowerCase();
|
INTERNAL_MCP_CLIENT_NAME.toLowerCase();
|
||||||
@@ -43,15 +42,20 @@ const inferClientNameFromRedirectUri = (redirectUri: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isInternalMcpOAuthClientRecord = ({
|
export const isInternalMcpOAuthClientRecord = ({
|
||||||
|
env,
|
||||||
clientId,
|
clientId,
|
||||||
name,
|
name,
|
||||||
metadata,
|
metadata,
|
||||||
}: {
|
}: {
|
||||||
|
env: Env;
|
||||||
clientId: string | null | undefined;
|
clientId: string | null | undefined;
|
||||||
name: string | null | undefined;
|
name: string | null | undefined;
|
||||||
metadata?: unknown;
|
metadata?: unknown;
|
||||||
}) => {
|
}) => {
|
||||||
if (INTERNAL_MCP_CLIENT_ID && clientId === INTERNAL_MCP_CLIENT_ID)
|
if (
|
||||||
|
env.INTERNAL_MCP_OAUTH_CLIENT_ID &&
|
||||||
|
clientId === env.INTERNAL_MCP_OAUTH_CLIENT_ID
|
||||||
|
)
|
||||||
return true;
|
return true;
|
||||||
if (name?.trim().toLowerCase() === INTERNAL_MCP_CLIENT_NAME_NORMALIZED) {
|
if (name?.trim().toLowerCase() === INTERNAL_MCP_CLIENT_NAME_NORMALIZED) {
|
||||||
return true;
|
return true;
|
||||||
@@ -79,20 +83,29 @@ export const getInternalMcpDisplayName = ({
|
|||||||
|
|
||||||
export const isInternalMcpOAuthClientId = async ({
|
export const isInternalMcpOAuthClientId = async ({
|
||||||
db,
|
db,
|
||||||
|
env,
|
||||||
clientId,
|
clientId,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
|
env: Env;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const client = await oauthClientRepo.getByClientId({ db, clientId });
|
const client = await oauthClientRepo.getByClientId({ db, clientId });
|
||||||
|
|
||||||
return isInternalMcpOAuthClientRecord(client ?? { clientId, name: null });
|
return isInternalMcpOAuthClientRecord({
|
||||||
|
env,
|
||||||
|
...(client ?? { clientId, name: null }),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleInternalMcpOAuthAuthorize = async (c: Context) => {
|
export const handleInternalMcpOAuthAuthorize = async (c: Context<HonoEnv>) => {
|
||||||
|
const auth = createAuth(c.env);
|
||||||
const url = new URL(c.req.raw.url);
|
const url = new URL(c.req.raw.url);
|
||||||
const clientId = url.searchParams.get("client_id");
|
const clientId = url.searchParams.get("client_id");
|
||||||
if (!clientId || !(await isInternalMcpOAuthClientId({ db, clientId }))) {
|
if (
|
||||||
|
!clientId ||
|
||||||
|
!(await isInternalMcpOAuthClientId({ db, env: c.env, clientId }))
|
||||||
|
) {
|
||||||
return auth.handler(c.req.raw);
|
return auth.handler(c.req.raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { stripOAuthTokenPrefix } from "@autumn/auth";
|
import { stripOAuthTokenPrefix } from "@autumn/auth";
|
||||||
import {
|
import {
|
||||||
AppEnv,
|
AppEnv,
|
||||||
@@ -19,21 +18,23 @@ import { oauthAccessTokenRepo, oauthConsentRepo } from "../repos/index.js";
|
|||||||
import { isAtmnOAuthClientId } from "./atmnOAuthClients.js";
|
import { isAtmnOAuthClientId } from "./atmnOAuthClients.js";
|
||||||
import { rotateOAuthConsentApiKey } from "./oauthConsentApiKey.js";
|
import { rotateOAuthConsentApiKey } from "./oauthConsentApiKey.js";
|
||||||
|
|
||||||
const getOAuthIssuer = () =>
|
const getOAuthIssuer = (env: Env) =>
|
||||||
`${runtimeEnv.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`;
|
`${env.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`;
|
||||||
|
|
||||||
const verifyResourceAccessToken = async ({
|
const verifyResourceAccessToken = async ({
|
||||||
|
env,
|
||||||
accessToken,
|
accessToken,
|
||||||
resource,
|
resource,
|
||||||
requestedScopes,
|
requestedScopes,
|
||||||
}: {
|
}: {
|
||||||
|
env: Env;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
resource: string | null;
|
resource: string | null;
|
||||||
requestedScopes: ScopeString[] | null;
|
requestedScopes: ScopeString[] | null;
|
||||||
}) => {
|
}) => {
|
||||||
if (!resource) return null;
|
if (!resource) return null;
|
||||||
|
|
||||||
const issuer = getOAuthIssuer();
|
const issuer = getOAuthIssuer(env);
|
||||||
try {
|
try {
|
||||||
const payload = await verifyAccessToken(accessToken, {
|
const payload = await verifyAccessToken(accessToken, {
|
||||||
jwksUrl: `${issuer}/jwks`,
|
jwksUrl: `${issuer}/jwks`,
|
||||||
@@ -52,11 +53,13 @@ const verifyResourceAccessToken = async ({
|
|||||||
|
|
||||||
export const getOAuthAccessTokenRecord = async ({
|
export const getOAuthAccessTokenRecord = async ({
|
||||||
db,
|
db,
|
||||||
|
env,
|
||||||
accessToken,
|
accessToken,
|
||||||
resource,
|
resource,
|
||||||
requestedScopes,
|
requestedScopes,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
|
env: Env;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
resource: string | null;
|
resource: string | null;
|
||||||
requestedScopes: ScopeString[] | null;
|
requestedScopes: ScopeString[] | null;
|
||||||
@@ -67,6 +70,7 @@ export const getOAuthAccessTokenRecord = async ({
|
|||||||
const tokenRecord =
|
const tokenRecord =
|
||||||
(await oauthAccessTokenRepo.getValidByTokenValues({ db, tokenValues })) ??
|
(await oauthAccessTokenRepo.getValidByTokenValues({ db, tokenValues })) ??
|
||||||
(await verifyResourceAccessToken({
|
(await verifyResourceAccessToken({
|
||||||
|
env,
|
||||||
accessToken: rawAccessToken,
|
accessToken: rawAccessToken,
|
||||||
resource,
|
resource,
|
||||||
requestedScopes,
|
requestedScopes,
|
||||||
@@ -120,10 +124,12 @@ export const getOAuthAccessTokenRecord = async ({
|
|||||||
|
|
||||||
export const getExternalOAuthApiKeyForToken = async ({
|
export const getExternalOAuthApiKeyForToken = async ({
|
||||||
db,
|
db,
|
||||||
|
env,
|
||||||
tokenRecord,
|
tokenRecord,
|
||||||
requestedScopes,
|
requestedScopes,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
|
env: Env;
|
||||||
tokenRecord: ResourceAccessTokenRecord & {
|
tokenRecord: ResourceAccessTokenRecord & {
|
||||||
userId: string;
|
userId: string;
|
||||||
referenceId: string;
|
referenceId: string;
|
||||||
@@ -132,6 +138,7 @@ export const getExternalOAuthApiKeyForToken = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const isAtmnClient = await isAtmnOAuthClientId({
|
const isAtmnClient = await isAtmnOAuthClientId({
|
||||||
db,
|
db,
|
||||||
|
env,
|
||||||
clientId: tokenRecord.clientId,
|
clientId: tokenRecord.clientId,
|
||||||
});
|
});
|
||||||
if (isAtmnClient) return null;
|
if (isAtmnClient) return null;
|
||||||
@@ -151,19 +158,19 @@ export const getExternalOAuthApiKeyForToken = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = consent.env ?? AppEnv.Sandbox;
|
const appEnv = consent.env ?? AppEnv.Sandbox;
|
||||||
const scopes = requestedScopes ?? tokenRecord.scopes;
|
const scopes = requestedScopes ?? tokenRecord.scopes;
|
||||||
const apiKey = await rotateOAuthConsentApiKey({
|
const apiKey = await rotateOAuthConsentApiKey({
|
||||||
db,
|
db,
|
||||||
consent,
|
consent,
|
||||||
tokenRecord,
|
tokenRecord,
|
||||||
env,
|
env: appEnv,
|
||||||
scopes,
|
scopes,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiKey,
|
apiKey,
|
||||||
env,
|
env: appEnv,
|
||||||
orgId: tokenRecord.referenceId,
|
orgId: tokenRecord.referenceId,
|
||||||
userId: tokenRecord.userId,
|
userId: tokenRecord.userId,
|
||||||
clientId: tokenRecord.clientId,
|
clientId: tokenRecord.clientId,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import {
|
import {
|
||||||
oauthProviderAuthServerMetadata,
|
oauthProviderAuthServerMetadata,
|
||||||
oauthProviderOpenIdConfigMetadata,
|
oauthProviderOpenIdConfigMetadata,
|
||||||
@@ -6,7 +5,7 @@ import {
|
|||||||
import { type Context, Hono } from "hono";
|
import { type Context, Hono } from "hono";
|
||||||
import { rateLimiter } from "hono-rate-limiter";
|
import { rateLimiter } from "hono-rate-limiter";
|
||||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
import { auth } from "@/utils/auth.js";
|
import { createAuth } from "@/utils/auth.js";
|
||||||
import { handleGetOAuthClient } from "./handleGetOAuthClient.js";
|
import { handleGetOAuthClient } from "./handleGetOAuthClient.js";
|
||||||
import { handleMcpOAuthRegistration } from "./handleMcpOAuthRegistration.js";
|
import { handleMcpOAuthRegistration } from "./handleMcpOAuthRegistration.js";
|
||||||
import { handleOAuthConsentWithEnv } from "./handleOAuthConsentWithEnv.js";
|
import { handleOAuthConsentWithEnv } from "./handleOAuthConsentWithEnv.js";
|
||||||
@@ -23,24 +22,28 @@ const getClientLookupRateLimitKey = (c: Context<HonoEnv>) =>
|
|||||||
|
|
||||||
const oauthClientLookupLimiter = rateLimiter<HonoEnv>({
|
const oauthClientLookupLimiter = rateLimiter<HonoEnv>({
|
||||||
windowMs: 60 * 1000,
|
windowMs: 60 * 1000,
|
||||||
limit: runtimeEnv.NODE_ENV === "development" ? 1000 : 60,
|
limit: (c) => (c.env.NODE_ENV === "development" ? 1000 : 60),
|
||||||
standardHeaders: "draft-6",
|
standardHeaders: "draft-6",
|
||||||
keyGenerator: getClientLookupRateLimitKey,
|
keyGenerator: getClientLookupRateLimitKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
oauthRouter.get("/api/auth/.well-known/openid-configuration", (c) => {
|
oauthRouter.get("/api/auth/.well-known/openid-configuration", (c) => {
|
||||||
|
const auth = createAuth(c.env);
|
||||||
return oauthProviderOpenIdConfigMetadata(auth)(c.req.raw);
|
return oauthProviderOpenIdConfigMetadata(auth)(c.req.raw);
|
||||||
});
|
});
|
||||||
|
|
||||||
oauthRouter.get("/.well-known/oauth-authorization-server", (c) => {
|
oauthRouter.get("/.well-known/oauth-authorization-server", (c) => {
|
||||||
|
const auth = createAuth(c.env);
|
||||||
return oauthProviderAuthServerMetadata(auth)(c.req.raw);
|
return oauthProviderAuthServerMetadata(auth)(c.req.raw);
|
||||||
});
|
});
|
||||||
|
|
||||||
oauthRouter.get("/api/auth/.well-known/oauth-authorization-server", (c) => {
|
oauthRouter.get("/api/auth/.well-known/oauth-authorization-server", (c) => {
|
||||||
|
const auth = createAuth(c.env);
|
||||||
return oauthProviderAuthServerMetadata(auth)(c.req.raw);
|
return oauthProviderAuthServerMetadata(auth)(c.req.raw);
|
||||||
});
|
});
|
||||||
|
|
||||||
oauthRouter.get("/.well-known/oauth-authorization-server/api/auth", (c) => {
|
oauthRouter.get("/.well-known/oauth-authorization-server/api/auth", (c) => {
|
||||||
|
const auth = createAuth(c.env);
|
||||||
return oauthProviderAuthServerMetadata(auth)(c.req.raw);
|
return oauthProviderAuthServerMetadata(auth)(c.req.raw);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { EventInsert } from "@autumn/shared";
|
import type { EventInsert } from "@autumn/shared";
|
||||||
import { logger } from "@server/external/logtail/logtailUtils.js";
|
import { createLogger } from "@server/external/logtail/logtailUtils.js";
|
||||||
import { sendEventsToTinybird } from "@server/external/tinybird/sendEvents/sendEvents.js";
|
import { sendEventsToTinybird } from "@server/external/tinybird/sendEvents/sendEvents.js";
|
||||||
import { JobName } from "@server/queue/JobName.js";
|
import { JobName } from "@server/queue/JobName.js";
|
||||||
import { addTaskToQueue } from "@server/queue/queueUtils.js";
|
import { addTaskToQueue } from "@server/queue/queueUtils.js";
|
||||||
@@ -11,13 +11,13 @@ class BatchingManager {
|
|||||||
private readonly maxBatchSize = 200; // Max events per batch (~200kb per event, keep batches under 10MB for Tinybird)
|
private readonly maxBatchSize = 200; // Max events per batch (~200kb per event, keep batches under 10MB for Tinybird)
|
||||||
|
|
||||||
/** Add an event to the batch */
|
/** Add an event to the batch */
|
||||||
addEvent(event: EventInsert): void {
|
addEvent(event: EventInsert, env: Env): void {
|
||||||
const key = event.id;
|
const key = event.id;
|
||||||
this.events.set(key, event);
|
this.events.set(key, event);
|
||||||
|
|
||||||
// Auto-execute if batch size is reached
|
// Auto-execute if batch size is reached
|
||||||
if (this.events.size >= this.maxBatchSize) {
|
if (this.events.size >= this.maxBatchSize) {
|
||||||
this.executeBatch();
|
this.executeBatch(env);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,12 +27,12 @@ class BatchingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.timer = setTimeout(() => {
|
this.timer = setTimeout(() => {
|
||||||
this.executeBatch();
|
this.executeBatch(env);
|
||||||
}, this.batchWindow);
|
}, this.batchWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Execute the current batch - queue to SQS for Postgres and send to Tinybird */
|
/** Execute the current batch - queue to SQS for Postgres and send to Tinybird */
|
||||||
private async executeBatch(): Promise<void> {
|
private async executeBatch(env: Env): Promise<void> {
|
||||||
if (this.timer) {
|
if (this.timer) {
|
||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
@@ -56,7 +56,7 @@ class BatchingManager {
|
|||||||
|
|
||||||
await sendEventsToTinybird({
|
await sendEventsToTinybird({
|
||||||
events: eventItems,
|
events: eventItems,
|
||||||
logger,
|
logger: createLogger(env),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ import type { ExpireLockReceiptPayload } from "@/queue/workflows.js";
|
|||||||
import { runFinalizeLock } from "./runFinalizeLock.js";
|
import { runFinalizeLock } from "./runFinalizeLock.js";
|
||||||
|
|
||||||
export const expireLock = async ({
|
export const expireLock = async ({
|
||||||
|
workerEnv,
|
||||||
ctx,
|
ctx,
|
||||||
payload,
|
payload,
|
||||||
}: {
|
}: {
|
||||||
|
workerEnv: Env;
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
payload: ExpireLockReceiptPayload;
|
payload: ExpireLockReceiptPayload;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
ctx.skipCache = false;
|
ctx.skipCache = false;
|
||||||
await runFinalizeLock({
|
await runFinalizeLock({
|
||||||
|
workerEnv,
|
||||||
ctx,
|
ctx,
|
||||||
params: {
|
params: {
|
||||||
lock_id: payload.lockId,
|
lock_id: payload.lockId,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { runFinalizeLockV2 } from "./runFinalizeLockV2.js";
|
|||||||
import { runRedisFinalizeLock } from "./runRedisFinalizeLock.js";
|
import { runRedisFinalizeLock } from "./runRedisFinalizeLock.js";
|
||||||
|
|
||||||
type RunFinalizeLockArgs = {
|
type RunFinalizeLockArgs = {
|
||||||
|
workerEnv: Env;
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: FinalizeLockParamsV0;
|
params: FinalizeLockParamsV0;
|
||||||
};
|
};
|
||||||
@@ -35,7 +36,11 @@ export const runFinalizeLock = async (args: RunFinalizeLockArgs) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const runFinalizeLockInner = async ({ ctx, params }: RunFinalizeLockArgs) => {
|
const runFinalizeLockInner = async ({
|
||||||
|
workerEnv,
|
||||||
|
ctx,
|
||||||
|
params,
|
||||||
|
}: RunFinalizeLockArgs) => {
|
||||||
const fetchedReceipt = await fetchLockReceipt({
|
const fetchedReceipt = await fetchLockReceipt({
|
||||||
ctx,
|
ctx,
|
||||||
lockId: params.lock_id,
|
lockId: params.lock_id,
|
||||||
@@ -43,6 +48,7 @@ const runFinalizeLockInner = async ({ ctx, params }: RunFinalizeLockArgs) => {
|
|||||||
|
|
||||||
if (fetchedReceipt.source === "redis_v2") {
|
if (fetchedReceipt.source === "redis_v2") {
|
||||||
return runFinalizeLockV2({
|
return runFinalizeLockV2({
|
||||||
|
workerEnv,
|
||||||
ctx,
|
ctx,
|
||||||
params,
|
params,
|
||||||
receipt: fetchedReceipt.receipt,
|
receipt: fetchedReceipt.receipt,
|
||||||
@@ -64,8 +70,9 @@ const runFinalizeLockInner = async ({ ctx, params }: RunFinalizeLockArgs) => {
|
|||||||
try {
|
try {
|
||||||
if (notNullish(receipt.expires_at)) {
|
if (notNullish(receipt.expires_at)) {
|
||||||
await cancelLockExpiry({
|
await cancelLockExpiry({
|
||||||
|
workerEnv,
|
||||||
orgId: ctx.org.id,
|
orgId: ctx.org.id,
|
||||||
env: ctx.env,
|
appEnv: ctx.env,
|
||||||
hashedKey: Bun.hash(params.lock_id).toString(),
|
hashedKey: Bun.hash(params.lock_id).toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { runRedisFinalizeLockV2 } from "./runRedisFinalizeLockV2.js";
|
|||||||
* marker key; `claimed === false` means another finalizer holds it.
|
* marker key; `claimed === false` means another finalizer holds it.
|
||||||
*/
|
*/
|
||||||
export const runFinalizeLockV2 = async ({
|
export const runFinalizeLockV2 = async ({
|
||||||
|
workerEnv,
|
||||||
ctx,
|
ctx,
|
||||||
params,
|
params,
|
||||||
receipt,
|
receipt,
|
||||||
@@ -27,6 +28,7 @@ export const runFinalizeLockV2 = async ({
|
|||||||
claimed,
|
claimed,
|
||||||
lockRedisInstance,
|
lockRedisInstance,
|
||||||
}: {
|
}: {
|
||||||
|
workerEnv: Env;
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: FinalizeLockParamsV0;
|
params: FinalizeLockParamsV0;
|
||||||
receipt: LockReceipt;
|
receipt: LockReceipt;
|
||||||
@@ -55,8 +57,9 @@ export const runFinalizeLockV2 = async ({
|
|||||||
try {
|
try {
|
||||||
if (notNullish(receipt.expires_at)) {
|
if (notNullish(receipt.expires_at)) {
|
||||||
await cancelLockExpiry({
|
await cancelLockExpiry({
|
||||||
|
workerEnv,
|
||||||
orgId: ctx.org.id,
|
orgId: ctx.org.id,
|
||||||
env: ctx.env,
|
appEnv: ctx.env,
|
||||||
hashedKey: Bun.hash(params.lock_id).toString(),
|
hashedKey: Bun.hash(params.lock_id).toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const handleFinalizeLock = createRoute({
|
|||||||
const ctx = c.get("ctx");
|
const ctx = c.get("ctx");
|
||||||
const params = c.req.valid("json");
|
const params = c.req.valid("json");
|
||||||
|
|
||||||
const response = await runFinalizeLock({ ctx, params });
|
const response = await runFinalizeLock({ workerEnv: c.env, ctx, params });
|
||||||
const status = ctx.extraLogs.finalizeLockFailedOpen ? 202 : 200;
|
const status = ctx.extraLogs.finalizeLockFailedOpen ? 202 : 200;
|
||||||
|
|
||||||
return c.json(response, status);
|
return c.json(response, status);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { ErrCode, RecaseError, type TrackParams } from "@autumn/shared";
|
import { ErrCode, RecaseError, type TrackParams } from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||||
import { queueTrack } from "./utils/queueTrack.js";
|
import { queueTrack } from "./utils/queueTrack.js";
|
||||||
@@ -13,7 +12,7 @@ export const runAsyncTrack = async ({
|
|||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
body: TrackParams;
|
body: TrackParams;
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const queueUrl = runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL;
|
const queueUrl = ctx.env.TRACK_ASYNC_SQS_QUEUE_URL;
|
||||||
if (!queueUrl) {
|
if (!queueUrl) {
|
||||||
ctx.logger.error(
|
ctx.logger.error(
|
||||||
"[track] async=true requested but TRACK_ASYNC_SQS_QUEUE_URL is unset",
|
"[track] async=true requested but TRACK_ASYNC_SQS_QUEUE_URL is unset",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { runtimeEnv } from "@/utils/envUtils.js";
|
|
||||||
import { type BatchTrackParams, ErrCode, RecaseError } from "@autumn/shared";
|
import { type BatchTrackParams, ErrCode, RecaseError } from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||||
import { JobName } from "@/queue/JobName.js";
|
import { JobName } from "@/queue/JobName.js";
|
||||||
@@ -16,7 +15,7 @@ export const runBatchTrack = async ({
|
|||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
body: BatchTrackParams;
|
body: BatchTrackParams;
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const queueUrl = runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL;
|
const queueUrl = ctx.env.TRACK_ASYNC_SQS_QUEUE_URL;
|
||||||
if (!queueUrl) {
|
if (!queueUrl) {
|
||||||
ctx.logger.error(
|
ctx.logger.error(
|
||||||
"[track] batch track requested but TRACK_ASYNC_SQS_QUEUE_URL is unset",
|
"[track] batch track requested but TRACK_ASYNC_SQS_QUEUE_URL is unset",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user