fix: approximated not verifying domain, change request type for /agents

route
This commit is contained in:
Rotimi Best
2026-05-21 14:05:37 +01:00
parent c1bbd02f8e
commit ec92fc5083
23 changed files with 179 additions and 176 deletions
+4
View File
@@ -10,6 +10,10 @@ const envSchema = z.object({
CLOUDFLARE_IMAGE_BUCKET_DOMAIN: z.string().optional(),
APPROXIMATED_API_KEY: z.string().optional(),
APPROXIMATED_TARGET_ADDRESS: z.string().optional(),
/** Public IP customers point their A record to (Approximated edge IP). */
APPROXIMATED_DNS_TARGET_IP: z.string().optional(),
/** Optional CNAME target customers can point to instead of A record. */
APPROXIMATED_DNS_TARGET_CNAME: z.string().optional(),
// S3-compatible storage (MinIO, AWS S3, or other S3-compatible backends)
OBJECT_STORAGE_ENDPOINT: z.string().optional(),
OBJECT_STORAGE_REGION: z.string().optional(),
@@ -0,0 +1,23 @@
import type { Context, Next } from 'hono';
import { AGENT_CONTENT_TYPE } from '@cio/utils/constants';
/**
* Render fronts services with Cloudflare's OWASP Managed Rules, which scan
* `application/json` bodies and 403 anything that looks like SQL injection.
* AI chat content trips this constantly (`SELECT … WHERE …`, `OR 1=1` etc.).
* The dashboard sends `/agent/*` writes as `application/x-cio-agent`; this
* middleware swaps the header back to `application/json` so Hono's zValidator
* (which gates parsing on a `/application\/.+?\+?json/` regex) parses the body.
*/
export const agentContentTypeRewrite = async (c: Context, next: Next) => {
const ct = c.req.header('content-type')?.toLowerCase() ?? '';
if (!ct.startsWith(AGENT_CONTENT_TYPE)) {
return next();
}
const headers = new Headers(c.req.raw.headers);
headers.set('content-type', 'application/json');
c.req.raw = new Request(c.req.raw, { headers });
return next();
};
+5 -1
View File
@@ -3,6 +3,7 @@ import { authMiddleware } from '@api/middlewares/auth';
import { orgMemberMiddleware } from '@api/middlewares/org-member';
import { orgAdminMiddleware } from '@api/middlewares/org-admin';
import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key';
import { agentContentTypeRewrite } from '@api/middlewares/agent-content-type';
import { handleError, AppError } from '@api/utils/errors';
import { zValidator } from '@hono/zod-validator';
import { streamText, stepCountIs, convertToModelMessages } from 'ai';
@@ -691,4 +692,7 @@ const agentCoreRouter = new Hono()
}
});
export const agentRouter = new Hono().route('/', agentCoreRouter).route('/history', agentHistoryRouter);
export const agentRouter = new Hono()
.use('*', agentContentTypeRewrite)
.route('/', agentCoreRouter)
.route('/history', agentHistoryRouter);
+33 -10
View File
@@ -57,6 +57,14 @@ function ensureApproximatedConfig() {
500
);
}
if (!env.APPROXIMATED_DNS_TARGET_IP && !env.APPROXIMATED_DNS_TARGET_CNAME) {
throw new AppError(
'Set APPROXIMATED_DNS_TARGET_IP and/or APPROXIMATED_DNS_TARGET_CNAME so customers know where to point their DNS.',
ErrorCodes.INTERNAL_ERROR,
500
);
}
}
async function approximatedRequest<T>(path: string, init: RequestInit = {}): Promise<T | null> {
@@ -92,20 +100,31 @@ async function approximatedRequest<T>(path: string, init: RequestInit = {}): Pro
);
}
if (payload && typeof payload === 'object' && 'data' in payload) {
return (payload as { data: T }).data;
}
return payload as T;
}
function buildDnsRecords(hostname: string, vhost?: ApproximatedVhost | null): DomainDnsRecord[] {
if (!vhost?.dns_pointed_at) return [];
const targetIp = env.APPROXIMATED_DNS_TARGET_IP;
const targetCname = env.APPROXIMATED_DNS_TARGET_CNAME;
return [
{
type: 'A',
name: hostname,
value: vhost.dns_pointed_at,
status: vhost.is_resolving ? 'active' : 'pending'
}
];
const records: DomainDnsRecord[] = [];
const isResolvingToTarget = Boolean(
vhost?.is_resolving && (vhost?.dns_pointed_at === targetIp || vhost?.dns_pointed_at === targetCname)
);
const status: 'pending' | 'active' = isResolvingToTarget ? 'active' : 'pending';
if (targetIp) {
records.push({ type: 'A', name: hostname, value: targetIp, status });
}
if (targetCname) {
records.push({ type: 'CNAME', name: hostname, value: targetCname, status });
}
return records;
}
function mapDomainStatus(vhost?: ApproximatedVhost | null): DomainSetupStatus {
@@ -113,7 +132,7 @@ function mapDomainStatus(vhost?: ApproximatedVhost | null): DomainSetupStatus {
return 'reconnect_required';
}
if (vhost.has_ssl && vhost.is_resolving && vhost.apx_hit) {
if (vhost.has_ssl && vhost.is_resolving) {
return 'verified';
}
@@ -145,6 +164,7 @@ function getStatusMessage(status: DomainSetupStatus) {
function toDomainSetupResult(hostname: string, vhost?: ApproximatedVhost | null): DomainSetupResult {
const status = mapDomainStatus(vhost);
console.log('status', status);
return {
hostname,
@@ -201,6 +221,8 @@ async function createVhost(hostname: string): Promise<ApproximatedVhost> {
if (!created) {
throw new AppError('Approximated did not return a vhost', ErrorCodes.INTERNAL_ERROR, 500);
}
console.log('created', created);
return created;
}
@@ -212,6 +234,7 @@ export async function connectDomain(hostname: string): Promise<DomainSetupResult
export async function refreshDomain(hostname: string): Promise<DomainSetupResult> {
const vhost = await getVhost(hostname);
console.log('existing', vhost);
return toDomainSetupResult(hostname, vhost);
}