feat: expand production config and documentation for provider integrations

This commit is contained in:
Codex
2026-02-18 13:35:19 +00:00
parent 74ab63f488
commit 415c9d165a
5 changed files with 217 additions and 6 deletions

View File

@@ -3,11 +3,35 @@ NODE_ENV=production
PORT=3000
STATE_FILE_PATH=/data/state.json
LOG_LEVEL=info
APP_BASE_URL=https://xartaudio.example.com
# Webhook secrets
X_WEBHOOK_SECRET=replace-me
POLAR_WEBHOOK_SECRET=replace-me
# X API
X_BEARER_TOKEN=replace-me
X_BOT_USER_ID=replace-me
# Polar API
POLAR_ACCESS_TOKEN=replace-me
POLAR_SERVER=production
POLAR_PRODUCT_IDS=prod_123
# TTS (OpenAI-compatible)
TTS_API_KEY=replace-me
TTS_BASE_URL=
TTS_MODEL=gpt-4o-mini-tts
TTS_VOICE=alloy
# S3-compatible object storage
S3_BUCKET=replace-me
S3_REGION=us-east-1
S3_ENDPOINT=
S3_ACCESS_KEY_ID=replace-me
S3_SECRET_ACCESS_KEY=replace-me
S3_SIGNED_URL_TTL_SEC=3600
# Credit policy
BASE_CREDITS=1
INCLUDED_CHARS=25000

View File

@@ -326,14 +326,22 @@ This repository now contains a deployable production-style app (single container
- non-owner pay-to-unlock (same credit amount, permanent unlock)
4. Webhook-first ingestion and billing:
- `POST /api/webhooks/x` (HMAC verified)
- `POST /api/webhooks/polar` (HMAC verified)
5. Persistent state across restarts:
- `POST /api/webhooks/polar` (supports Polar standard webhook signatures and legacy HMAC fallback)
5. Real integration adapters implemented:
- X API (`twitter-api-v2`)
- Polar SDK checkout/webhook handling (`@polar-sh/sdk`)
- TTS (`openai`)
- Object storage + signed URLs (`@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner`)
6. Persistent state across restarts:
- all wallet/job/asset/access state is snapshotted and stored to `STATE_FILE_PATH`
6. Abuse protection:
7. Abuse protection:
- fixed-window rate limiting for webhook, auth, and action routes
7. PWA support:
8. PWA support:
- `manifest.webmanifest`
- `sw.js`
9. Bun-native quality checks:
- `bun test`
- `bun run lint`
### Authentication model
1. Browser flow uses secure-ish HTTP-only cookie session (`xartaudio_user`) via `/auth/dev-login`.
@@ -354,6 +362,8 @@ This repository now contains a deployable production-style app (single container
3. APIs:
- `POST /api/webhooks/x`
- `POST /api/webhooks/polar`
- `POST /api/payments/create-checkout`
- `GET /api/x/mentions`
- `GET /api/me/wallet`
- `GET /api/jobs/:id`
- `POST /api/audio/:id/unlock`
@@ -371,9 +381,26 @@ Use `.env.example` as the source of truth.
1. Runtime:
- `PORT`
- `STATE_FILE_PATH`
- `LOG_LEVEL`
- `APP_BASE_URL`
2. Secrets:
- `X_WEBHOOK_SECRET`
- `POLAR_WEBHOOK_SECRET`
- `X_BEARER_TOKEN`
- `X_BOT_USER_ID`
- `POLAR_ACCESS_TOKEN`
- `POLAR_SERVER`
- `POLAR_PRODUCT_IDS`
- `TTS_API_KEY`
- `TTS_BASE_URL`
- `TTS_MODEL`
- `TTS_VOICE`
- `S3_BUCKET`
- `S3_REGION`
- `S3_ENDPOINT`
- `S3_ACCESS_KEY_ID`
- `S3_SECRET_ACCESS_KEY`
- `S3_SIGNED_URL_TTL_SEC`
3. Credit model:
- `BASE_CREDITS`
- `INCLUDED_CHARS`
@@ -400,6 +427,6 @@ Use `.env.example` as the source of truth.
## Production Checklist
1. Replace dev-login cookie auth with X OAuth before public launch.
2. Connect real TTS generation worker and object storage (S3/R2/GCS).
2. Populate integration keys in Coolify environment for X, Polar, TTS, and S3.
3. Replace local state file with managed database for multi-replica scaling.
4. Add structured logging, tracing, and external alerting.
4. Add tracing and external alerting.

View File

@@ -19,12 +19,40 @@ function strFromEnv(name, fallback) {
return raw && String(raw).trim() ? String(raw).trim() : fallback;
}
function listFromEnv(name, fallback = []) {
const raw = process.env[name];
if (!raw) {
return fallback;
}
return String(raw)
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
const parsed = {
port: intFromEnv("PORT", 3000),
stateFilePath: strFromEnv("STATE_FILE_PATH", "./data/state.json"),
logLevel: strFromEnv("LOG_LEVEL", "info"),
appBaseUrl: strFromEnv("APP_BASE_URL", "http://localhost:3000"),
xWebhookSecret: process.env.X_WEBHOOK_SECRET || "dev-x-secret",
xBearerToken: strFromEnv("X_BEARER_TOKEN", ""),
xBotUserId: strFromEnv("X_BOT_USER_ID", ""),
polarWebhookSecret: process.env.POLAR_WEBHOOK_SECRET || "dev-polar-secret",
polarAccessToken: strFromEnv("POLAR_ACCESS_TOKEN", ""),
polarServer: strFromEnv("POLAR_SERVER", "production"),
polarProductIds: listFromEnv("POLAR_PRODUCT_IDS", []),
ttsApiKey: strFromEnv("TTS_API_KEY", ""),
ttsBaseUrl: strFromEnv("TTS_BASE_URL", ""),
ttsModel: strFromEnv("TTS_MODEL", "gpt-4o-mini-tts"),
ttsVoice: strFromEnv("TTS_VOICE", "alloy"),
s3Bucket: strFromEnv("S3_BUCKET", ""),
s3Region: strFromEnv("S3_REGION", ""),
s3Endpoint: strFromEnv("S3_ENDPOINT", ""),
s3AccessKeyId: strFromEnv("S3_ACCESS_KEY_ID", ""),
s3SecretAccessKey: strFromEnv("S3_SECRET_ACCESS_KEY", ""),
s3SignedUrlTtlSec: intFromEnv("S3_SIGNED_URL_TTL_SEC", 3600),
rateLimits: {
webhookPerMinute: intFromEnv("WEBHOOK_RPM", 120),
authPerMinute: intFromEnv("AUTH_RPM", 30),
@@ -43,8 +71,24 @@ const ConfigSchema = z.object({
port: z.number().int().positive(),
stateFilePath: z.string().min(1),
logLevel: z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]),
appBaseUrl: z.string().min(1),
xWebhookSecret: z.string().min(1),
xBearerToken: z.string(),
xBotUserId: z.string(),
polarWebhookSecret: z.string().min(1),
polarAccessToken: z.string(),
polarServer: z.enum(["production", "sandbox"]),
polarProductIds: z.array(z.string().min(1)),
ttsApiKey: z.string(),
ttsBaseUrl: z.string(),
ttsModel: z.string().min(1),
ttsVoice: z.string().min(1),
s3Bucket: z.string(),
s3Region: z.string(),
s3Endpoint: z.string(),
s3AccessKeyId: z.string(),
s3SecretAccessKey: z.string(),
s3SignedUrlTtlSec: z.number().int().positive(),
rateLimits: z.object({
webhookPerMinute: z.number().int().positive(),
authPerMinute: z.number().int().positive(),

View File

@@ -8,13 +8,25 @@ test("config uses defaults when env is missing", () => {
PORT: process.env.PORT,
STATE_FILE_PATH: process.env.STATE_FILE_PATH,
LOG_LEVEL: process.env.LOG_LEVEL,
APP_BASE_URL: process.env.APP_BASE_URL,
TTS_MODEL: process.env.TTS_MODEL,
S3_SIGNED_URL_TTL_SEC: process.env.S3_SIGNED_URL_TTL_SEC,
X_BOT_USER_ID: process.env.X_BOT_USER_ID,
WEBHOOK_RPM: process.env.WEBHOOK_RPM,
POLAR_SERVER: process.env.POLAR_SERVER,
POLAR_PRODUCT_IDS: process.env.POLAR_PRODUCT_IDS,
};
delete process.env.PORT;
delete process.env.STATE_FILE_PATH;
delete process.env.LOG_LEVEL;
delete process.env.APP_BASE_URL;
delete process.env.TTS_MODEL;
delete process.env.S3_SIGNED_URL_TTL_SEC;
delete process.env.X_BOT_USER_ID;
delete process.env.WEBHOOK_RPM;
delete process.env.POLAR_SERVER;
delete process.env.POLAR_PRODUCT_IDS;
delete require.cache[require.resolve("../src/config")];
const { config } = require("../src/config");
@@ -22,6 +34,12 @@ test("config uses defaults when env is missing", () => {
assert.equal(config.port, 3000);
assert.equal(config.stateFilePath, "./data/state.json");
assert.equal(config.logLevel, "info");
assert.equal(config.appBaseUrl, "http://localhost:3000");
assert.equal(config.ttsModel, "gpt-4o-mini-tts");
assert.equal(config.s3SignedUrlTtlSec, 3600);
assert.equal(config.xBotUserId, "");
assert.equal(config.polarServer, "production");
assert.deepEqual(config.polarProductIds, []);
assert.equal(config.rateLimits.webhookPerMinute, 120);
if (previous.PORT === undefined) {
@@ -42,11 +60,47 @@ test("config uses defaults when env is missing", () => {
process.env.LOG_LEVEL = previous.LOG_LEVEL;
}
if (previous.APP_BASE_URL === undefined) {
delete process.env.APP_BASE_URL;
} else {
process.env.APP_BASE_URL = previous.APP_BASE_URL;
}
if (previous.TTS_MODEL === undefined) {
delete process.env.TTS_MODEL;
} else {
process.env.TTS_MODEL = previous.TTS_MODEL;
}
if (previous.S3_SIGNED_URL_TTL_SEC === undefined) {
delete process.env.S3_SIGNED_URL_TTL_SEC;
} else {
process.env.S3_SIGNED_URL_TTL_SEC = previous.S3_SIGNED_URL_TTL_SEC;
}
if (previous.X_BOT_USER_ID === undefined) {
delete process.env.X_BOT_USER_ID;
} else {
process.env.X_BOT_USER_ID = previous.X_BOT_USER_ID;
}
if (previous.WEBHOOK_RPM === undefined) {
delete process.env.WEBHOOK_RPM;
} else {
process.env.WEBHOOK_RPM = previous.WEBHOOK_RPM;
}
if (previous.POLAR_SERVER === undefined) {
delete process.env.POLAR_SERVER;
} else {
process.env.POLAR_SERVER = previous.POLAR_SERVER;
}
if (previous.POLAR_PRODUCT_IDS === undefined) {
delete process.env.POLAR_PRODUCT_IDS;
} else {
process.env.POLAR_PRODUCT_IDS = previous.POLAR_PRODUCT_IDS;
}
});
test("config reads state path and numeric env overrides", () => {
@@ -55,12 +109,24 @@ test("config reads state path and numeric env overrides", () => {
STATE_FILE_PATH: process.env.STATE_FILE_PATH,
LOG_LEVEL: process.env.LOG_LEVEL,
WEBHOOK_RPM: process.env.WEBHOOK_RPM,
APP_BASE_URL: process.env.APP_BASE_URL,
TTS_MODEL: process.env.TTS_MODEL,
S3_SIGNED_URL_TTL_SEC: process.env.S3_SIGNED_URL_TTL_SEC,
X_BOT_USER_ID: process.env.X_BOT_USER_ID,
POLAR_SERVER: process.env.POLAR_SERVER,
POLAR_PRODUCT_IDS: process.env.POLAR_PRODUCT_IDS,
};
process.env.PORT = "8080";
process.env.STATE_FILE_PATH = "/data/prod-state.json";
process.env.LOG_LEVEL = "debug";
process.env.APP_BASE_URL = "https://xartaudio.app";
process.env.TTS_MODEL = "custom-tts";
process.env.S3_SIGNED_URL_TTL_SEC = "7200";
process.env.X_BOT_USER_ID = "bot-user-id";
process.env.WEBHOOK_RPM = "77";
process.env.POLAR_SERVER = "sandbox";
process.env.POLAR_PRODUCT_IDS = "prod_1,prod_2";
delete require.cache[require.resolve("../src/config")];
const { config } = require("../src/config");
@@ -68,6 +134,12 @@ test("config reads state path and numeric env overrides", () => {
assert.equal(config.port, 8080);
assert.equal(config.stateFilePath, "/data/prod-state.json");
assert.equal(config.logLevel, "debug");
assert.equal(config.appBaseUrl, "https://xartaudio.app");
assert.equal(config.ttsModel, "custom-tts");
assert.equal(config.s3SignedUrlTtlSec, 7200);
assert.equal(config.xBotUserId, "bot-user-id");
assert.equal(config.polarServer, "sandbox");
assert.deepEqual(config.polarProductIds, ["prod_1", "prod_2"]);
assert.equal(config.rateLimits.webhookPerMinute, 77);
if (previous.PORT === undefined) {
@@ -86,9 +158,46 @@ test("config reads state path and numeric env overrides", () => {
} else {
process.env.LOG_LEVEL = previous.LOG_LEVEL;
}
if (previous.APP_BASE_URL === undefined) {
delete process.env.APP_BASE_URL;
} else {
process.env.APP_BASE_URL = previous.APP_BASE_URL;
}
if (previous.TTS_MODEL === undefined) {
delete process.env.TTS_MODEL;
} else {
process.env.TTS_MODEL = previous.TTS_MODEL;
}
if (previous.S3_SIGNED_URL_TTL_SEC === undefined) {
delete process.env.S3_SIGNED_URL_TTL_SEC;
} else {
process.env.S3_SIGNED_URL_TTL_SEC = previous.S3_SIGNED_URL_TTL_SEC;
}
if (previous.X_BOT_USER_ID === undefined) {
delete process.env.X_BOT_USER_ID;
} else {
process.env.X_BOT_USER_ID = previous.X_BOT_USER_ID;
}
if (previous.WEBHOOK_RPM === undefined) {
delete process.env.WEBHOOK_RPM;
} else {
process.env.WEBHOOK_RPM = previous.WEBHOOK_RPM;
}
if (previous.POLAR_SERVER === undefined) {
delete process.env.POLAR_SERVER;
} else {
process.env.POLAR_SERVER = previous.POLAR_SERVER;
}
if (previous.POLAR_PRODUCT_IDS === undefined) {
delete process.env.POLAR_PRODUCT_IDS;
} else {
process.env.POLAR_PRODUCT_IDS = previous.POLAR_PRODUCT_IDS;
}
});

View File

@@ -17,8 +17,15 @@ test("Dockerfile contains production container essentials", () => {
test("env example includes required webhook and credit settings", () => {
const envFile = fs.readFileSync(".env.example", "utf8");
assert.match(envFile, /LOG_LEVEL=/);
assert.match(envFile, /APP_BASE_URL=/);
assert.match(envFile, /X_WEBHOOK_SECRET=/);
assert.match(envFile, /X_BEARER_TOKEN=/);
assert.match(envFile, /X_BOT_USER_ID=/);
assert.match(envFile, /POLAR_WEBHOOK_SECRET=/);
assert.match(envFile, /POLAR_ACCESS_TOKEN=/);
assert.match(envFile, /POLAR_PRODUCT_IDS=/);
assert.match(envFile, /TTS_API_KEY=/);
assert.match(envFile, /S3_BUCKET=/);
assert.match(envFile, /INCLUDED_CHARS=/);
assert.match(envFile, /WEBHOOK_RPM=/);
});