ldrouter 1.16.2 → 1.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -38
- package/README.md +1 -23
- package/dist/server/app.js +1 -10
- package/dist/server/auth/middleware.js +1 -37
- package/dist/server/db/index.js +0 -5
- package/dist/server/db/migrate.js +5 -38
- package/dist/server/db/schema.js +3 -44
- package/dist/server/errors.js +11 -0
- package/dist/server/gateway/runner.js +64 -126
- package/dist/server/protocols/anthropic.js +5 -3
- package/dist/server/protocols/canonical.js +29 -8
- package/dist/server/providers/index.js +25 -8
- package/dist/server/routes/admin/auth.js +1 -4
- package/dist/server/routes/admin/combos.js +98 -48
- package/dist/server/routes/admin/models.js +34 -24
- package/dist/server/routes/admin/providers.js +20 -63
- package/dist/server/routes/admin/requests.js +0 -1
- package/dist/server/routes/admin.js +0 -12
- package/dist/server/routes/gateway/anthropic.js +3 -3
- package/dist/server/routes/gateway/openai.js +5 -5
- package/dist/server/routing/capabilities.js +73 -14
- package/dist/server/routing/combo.js +20 -39
- package/dist/server/routing/resolver.js +16 -11
- package/dist/server/upstream/client.js +55 -61
- package/dist/web/assets/index-C5h2WXK5.css +1 -0
- package/dist/web/assets/index-CRnoua24.js +335 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -5
- package/dist/server/db/repositories/codex-accounts.js +0 -187
- package/dist/server/providers/codex-autostart.js +0 -98
- package/dist/server/providers/codex-import.js +0 -156
- package/dist/server/providers/codex-oauth.js +0 -77
- package/dist/server/providers/codex-refresh.js +0 -165
- package/dist/server/providers/codex-usage.js +0 -192
- package/dist/server/providers/codex.js +0 -186
- package/dist/server/routes/admin/codex.js +0 -331
- package/dist/web/assets/index-Coy-u6h8.css +0 -1
- package/dist/web/assets/index-qDG5c6aL.js +0 -386
- package/migrations/0005_codex_accounts.sql +0 -105
- package/migrations/0006_codex_usage.sql +0 -9
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { GatewayError } from '../../errors.js';
|
|
3
|
-
import { requireAdminAuth, requireAdminCsrf } from '../../auth/middleware.js';
|
|
4
|
-
import { recordAudit } from '../../db/repositories/audit.js';
|
|
5
|
-
import { listCodexAccountSummaries, setCodexAccountHealth, toCodexAccountSummaryRow, upsertCodexAccount, findCodexAccountForImport, } from '../../db/repositories/codex-accounts.js';
|
|
6
|
-
import { getRawDb } from '../../db/index.js';
|
|
7
|
-
import { parseCodexImportText, toCodexPreview } from '../../providers/codex-import.js';
|
|
8
|
-
import { probeCodex } from '../../providers/codex.js';
|
|
9
|
-
import { fetchCodexResetCredits, consumeCodexResetCredit } from '../../providers/codex-usage.js';
|
|
10
|
-
import { codexCredentialError, withCodexCredentials } from '../../providers/codex-refresh.js';
|
|
11
|
-
import { buildCodexAuthorizeUrl, exchangeCodexCode, extractCodeFromCallback, newCodexPkce } from '../../providers/codex-oauth.js';
|
|
12
|
-
import { refreshStoredCodexUsage } from '../../providers/codex-autostart.js';
|
|
13
|
-
import { redactString } from '../../security/redact.js';
|
|
14
|
-
const MAX_BYTES = 2_000_000;
|
|
15
|
-
const MAX_RECORDS = 500;
|
|
16
|
-
const ImportBody = z.object({ providerId: z.string().min(1), selectedIndexes: z.array(z.number().int().min(0)).max(MAX_RECORDS).optional() });
|
|
17
|
-
const MutationOptions = { preHandler: requireAdminCsrf };
|
|
18
|
-
export const CodexAccountUpdate = z.object({
|
|
19
|
-
enabled: z.boolean().optional(),
|
|
20
|
-
email: z.string().email().nullable().optional(),
|
|
21
|
-
workspaceId: z.string().max(256).nullable().optional(),
|
|
22
|
-
planType: z.string().max(128).nullable().optional(),
|
|
23
|
-
priority: z.number().int().min(0).max(100000).optional(),
|
|
24
|
-
autostart: z.boolean().optional(),
|
|
25
|
-
}).refine((v) => Object.keys(v).length > 0, 'At least one update is required');
|
|
26
|
-
/** Credential failures must surface as typed admin errors, not an opaque 500. */
|
|
27
|
-
async function withCredentials(accountId, fn) {
|
|
28
|
-
try {
|
|
29
|
-
return await withCodexCredentials(accountId, fn);
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
throw codexCredentialError(error);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function parseAll(text) {
|
|
36
|
-
const result = parseCodexImportText(text);
|
|
37
|
-
const records = [];
|
|
38
|
-
const failures = [];
|
|
39
|
-
for (const item of result)
|
|
40
|
-
('accessToken' in item ? records : failures).push(item);
|
|
41
|
-
return { records, failures };
|
|
42
|
-
}
|
|
43
|
-
function providerOrThrow(providerId) {
|
|
44
|
-
const provider = getRawDb().prepare('SELECT id,type,base_url,total_timeout_ms FROM providers WHERE id=?').get(providerId);
|
|
45
|
-
if (!provider)
|
|
46
|
-
throw new GatewayError('invalid_request_error', 'Provider not found', { status: 404 });
|
|
47
|
-
if (provider.type !== 'codex')
|
|
48
|
-
throw new GatewayError('invalid_request_error', 'Provider must be Codex', { status: 400 });
|
|
49
|
-
return provider;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Desktop/e2e flows cannot always hand a real File to a multipart file part, so a file part whose
|
|
53
|
-
* name lacks a text extension is accepted as base64 of the JSON payload.
|
|
54
|
-
*/
|
|
55
|
-
const TEXT_EXT = /\.(json|jsonl|txt)$/i;
|
|
56
|
-
async function readInput(req, body) {
|
|
57
|
-
const contentType = String(req.headers['content-type'] ?? '');
|
|
58
|
-
if (contentType.includes('multipart/form-data')) {
|
|
59
|
-
const parts = req.parts();
|
|
60
|
-
let providerId = '';
|
|
61
|
-
let selectedIndexes;
|
|
62
|
-
const texts = [];
|
|
63
|
-
let bytes = 0;
|
|
64
|
-
for await (const part of parts) {
|
|
65
|
-
if (part.type === 'file') {
|
|
66
|
-
const chunks = [];
|
|
67
|
-
for await (const chunk of part.file) {
|
|
68
|
-
bytes += chunk.length;
|
|
69
|
-
if (bytes > MAX_BYTES)
|
|
70
|
-
throw new GatewayError('invalid_request_error', 'Import exceeds maximum size', { status: 413 });
|
|
71
|
-
chunks.push(chunk);
|
|
72
|
-
}
|
|
73
|
-
const raw = Buffer.concat(chunks).toString('utf8');
|
|
74
|
-
texts.push(TEXT_EXT.test(part.filename ?? '') ? raw : Buffer.from(raw, 'base64').toString('utf8'));
|
|
75
|
-
}
|
|
76
|
-
else if (part.fieldname === 'providerId')
|
|
77
|
-
providerId = String(part.value);
|
|
78
|
-
else if (part.fieldname === 'selectedIndexes') {
|
|
79
|
-
let parsedIndexes;
|
|
80
|
-
try {
|
|
81
|
-
parsedIndexes = JSON.parse(String(part.value));
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
throw new GatewayError('invalid_request_error', 'selectedIndexes must be JSON', { status: 400 });
|
|
85
|
-
}
|
|
86
|
-
selectedIndexes = z.array(z.number().int().min(0)).max(MAX_RECORDS).parse(parsedIndexes);
|
|
87
|
-
}
|
|
88
|
-
else if (part.fieldname === 'text')
|
|
89
|
-
texts.push(String(part.value));
|
|
90
|
-
}
|
|
91
|
-
return { providerId, text: texts.join('\n'), selectedIndexes };
|
|
92
|
-
}
|
|
93
|
-
const parsed = ImportBody.extend({ text: z.union([z.string(), z.record(z.string(), z.unknown()), z.array(z.unknown())]) }).parse(body);
|
|
94
|
-
const text = typeof parsed.text === 'string' ? parsed.text : JSON.stringify(parsed.text);
|
|
95
|
-
if (Buffer.byteLength(text, 'utf8') > MAX_BYTES)
|
|
96
|
-
throw new GatewayError('invalid_request_error', 'Import exceeds maximum size', { status: 413 });
|
|
97
|
-
return { providerId: parsed.providerId, text, selectedIndexes: parsed.selectedIndexes };
|
|
98
|
-
}
|
|
99
|
-
const selectAccount = "SELECT id,email,workspace_id AS workspaceId,chatgpt_account_id AS chatgptAccountId,plan_type AS planType,token_expires_at AS tokenExpiresAt,enabled,health_state AS healthState,last_refresh_at AS lastRefreshAt,priority,codex_autostart_enabled AS autostart,codex_usage_json AS usageJson,codex_usage_error AS usageError,codex_usage_updated_at AS usageUpdatedAt,last_pinged_reset_at AS lastPingedResetAt,last_ping_at AS lastPingAt,created_at AS createdAt,updated_at AS updatedAt FROM codex_accounts WHERE id=?";
|
|
100
|
-
function accountById(id) {
|
|
101
|
-
const row = getRawDb().prepare(selectAccount).get(id);
|
|
102
|
-
if (!row)
|
|
103
|
-
throw new GatewayError('invalid_request_error', 'Codex account not found', { status: 404 });
|
|
104
|
-
return row;
|
|
105
|
-
}
|
|
106
|
-
/** Narrow seam onto the Codex account row so quota routes can read provider + identity. */
|
|
107
|
-
function accountTarget(id) {
|
|
108
|
-
const row = getRawDb().prepare('SELECT provider_id AS providerId, chatgpt_account_id AS accountId FROM codex_accounts WHERE id=?').get(id);
|
|
109
|
-
if (!row)
|
|
110
|
-
throw new GatewayError('invalid_request_error', 'Codex account not found', { status: 404 });
|
|
111
|
-
return { ...row, provider: providerOrThrow(row.providerId) };
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Server-held PKCE sessions. The challenge must derive from the verifier used at exchange time,
|
|
115
|
-
* so the verifier never leaves the process; the dialog polls this state instead.
|
|
116
|
-
*/
|
|
117
|
-
const oauthSessions = new Map();
|
|
118
|
-
const OAUTH_TTL_MS = 15 * 60_000;
|
|
119
|
-
const reapOauthSessions = () => {
|
|
120
|
-
const cutoff = Date.now() - OAUTH_TTL_MS;
|
|
121
|
-
for (const [state, session] of oauthSessions)
|
|
122
|
-
if (session.createdAt < cutoff)
|
|
123
|
-
oauthSessions.delete(state);
|
|
124
|
-
};
|
|
125
|
-
/**
|
|
126
|
-
* Loopback capture endpoint for the Codex CLI redirect (http://localhost:1455/auth/callback).
|
|
127
|
-
* Registered outside the admin scope because the browser's redirect carries no session; the code
|
|
128
|
-
* is held in memory against its state and exchanged only by /oauth/complete.
|
|
129
|
-
*/
|
|
130
|
-
export async function registerCodexOAuthCallbackRoute(app) {
|
|
131
|
-
app.get('/oauth/codex/callback', async (req, reply) => {
|
|
132
|
-
const { code, state, error } = req.query;
|
|
133
|
-
reapOauthSessions();
|
|
134
|
-
const session = state ? oauthSessions.get(state) : undefined;
|
|
135
|
-
if (session && code)
|
|
136
|
-
session.code = code;
|
|
137
|
-
const failed = Boolean(error) || !session || !code;
|
|
138
|
-
reply.type('text/html').send(`<!doctype html><meta charset="utf-8"><title>LateDev Router</title><body style="font:14px system-ui;padding:2rem">` +
|
|
139
|
-
(failed
|
|
140
|
-
? `<h1>Authorization failed</h1><p>${session ? 'No authorization code was returned. Close this tab and try again.' : 'This authorization session is unknown or has expired.'}</p>`
|
|
141
|
-
: `<h1>Account connected</h1><p>You can close this tab and return to LateDev Router.</p>`));
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
export async function registerCodexRoutes(app) {
|
|
145
|
-
app.addHook('preHandler', requireAdminAuth);
|
|
146
|
-
app.post('/api/admin/codex/accounts/preview', { preHandler: requireAdminCsrf }, async (req) => {
|
|
147
|
-
const input = await readInput(req, req.body);
|
|
148
|
-
providerOrThrow(input.providerId);
|
|
149
|
-
const parsed = parseAll(input.text);
|
|
150
|
-
const seen = new Map();
|
|
151
|
-
const records = parsed.records.map((record) => {
|
|
152
|
-
const duplicateOf = seen.get(record.identity);
|
|
153
|
-
seen.set(record.identity, record.index);
|
|
154
|
-
const existing = findCodexAccountForImport(input.providerId, { chatgptAccountId: record.chatgptAccountId, workspaceId: record.workspaceId, email: record.email, tokenDigest: '' });
|
|
155
|
-
return { ...toCodexPreview(record, duplicateOf === undefined ? (existing?.id ?? null) : String(duplicateOf)), duplicateOf: duplicateOf === undefined ? (existing?.id ?? null) : String(duplicateOf) };
|
|
156
|
-
});
|
|
157
|
-
return { records: [...records, ...parsed.failures.map((f) => ({ index: f.index, source: f.source, valid: false, error: 'Invalid record' }))], validCount: records.length, invalidCount: parsed.failures.length };
|
|
158
|
-
});
|
|
159
|
-
app.post('/api/admin/codex/accounts/import', MutationOptions, async (req) => {
|
|
160
|
-
const input = await readInput(req, req.body);
|
|
161
|
-
providerOrThrow(input.providerId);
|
|
162
|
-
const parsed = parseAll(input.text);
|
|
163
|
-
const selected = input.selectedIndexes ? new Set(input.selectedIndexes) : null;
|
|
164
|
-
const results = [];
|
|
165
|
-
let added = 0;
|
|
166
|
-
let updated = 0;
|
|
167
|
-
let skipped = 0;
|
|
168
|
-
let failed = 0;
|
|
169
|
-
for (const failure of parsed.failures) {
|
|
170
|
-
if (!selected || selected.has(failure.index)) {
|
|
171
|
-
failed++;
|
|
172
|
-
results.push({ index: failure.index, status: 'failed', error: 'Invalid record' });
|
|
173
|
-
}
|
|
174
|
-
else
|
|
175
|
-
skipped++;
|
|
176
|
-
}
|
|
177
|
-
for (const record of parsed.records) {
|
|
178
|
-
if (selected && !selected.has(record.index)) {
|
|
179
|
-
skipped++;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
const result = upsertCodexAccount(input.providerId, record);
|
|
184
|
-
if (result.status === 'added')
|
|
185
|
-
added++;
|
|
186
|
-
else
|
|
187
|
-
updated++;
|
|
188
|
-
results.push({ index: record.index, status: result.status, email: record.email, accountIdMasked: toCodexPreview(record).accountIdMasked });
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
failed++;
|
|
192
|
-
results.push({ index: record.index, status: 'failed', error: 'Import failed' });
|
|
193
|
-
void error;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
recordAudit({ action: 'codex.accounts.import', success: failed === 0, targetType: 'provider', targetId: input.providerId, ip: req.ip, metadata: { added, updated, skipped, failed } });
|
|
197
|
-
return { added, updated, skipped, failed, results };
|
|
198
|
-
});
|
|
199
|
-
app.get('/api/admin/codex/accounts', async (req) => {
|
|
200
|
-
const providerId = z.object({ providerId: z.string().min(1) }).parse(req.query).providerId;
|
|
201
|
-
providerOrThrow(providerId);
|
|
202
|
-
return { accounts: listCodexAccountSummaries(providerId) };
|
|
203
|
-
});
|
|
204
|
-
/** Drag-and-drop routing order. The posted array order becomes priority 0..n-1. */
|
|
205
|
-
const ReorderBody = z.object({ providerId: z.string().min(1), ids: z.array(z.string().min(1)).min(1).max(MAX_RECORDS) });
|
|
206
|
-
app.post('/api/admin/codex/accounts/reorder', MutationOptions, async (req) => {
|
|
207
|
-
const { providerId, ids } = ReorderBody.parse(req.body);
|
|
208
|
-
providerOrThrow(providerId);
|
|
209
|
-
const owned = new Set(listCodexAccountSummaries(providerId).map((account) => account.id));
|
|
210
|
-
if (ids.some((id) => !owned.has(id)))
|
|
211
|
-
throw new GatewayError('invalid_request_error', 'Account list does not match the provider', { status: 400 });
|
|
212
|
-
const raw = getRawDb();
|
|
213
|
-
const now = new Date().toISOString();
|
|
214
|
-
const update = raw.prepare('UPDATE codex_accounts SET priority=?, updated_at=? WHERE id=?');
|
|
215
|
-
raw.transaction(() => ids.forEach((id, index) => update.run(index, now, id)))();
|
|
216
|
-
recordAudit({ action: 'codex.accounts.reorder', success: true, targetType: 'provider', targetId: providerId, ip: req.ip, metadata: { count: ids.length } });
|
|
217
|
-
return { accounts: listCodexAccountSummaries(providerId) };
|
|
218
|
-
});
|
|
219
|
-
app.post('/api/admin/codex/oauth/start', MutationOptions, async (req) => {
|
|
220
|
-
const { providerId } = z.object({ providerId: z.string().min(1) }).parse(req.body);
|
|
221
|
-
providerOrThrow(providerId);
|
|
222
|
-
reapOauthSessions();
|
|
223
|
-
const { verifier, state } = newCodexPkce();
|
|
224
|
-
oauthSessions.set(state, { verifier, providerId, createdAt: Date.now() });
|
|
225
|
-
return { state, authorizeUrl: buildCodexAuthorizeUrl(verifier, state) };
|
|
226
|
-
});
|
|
227
|
-
app.get('/api/admin/codex/oauth/:state', async (req) => {
|
|
228
|
-
const { state } = req.params;
|
|
229
|
-
reapOauthSessions();
|
|
230
|
-
const session = oauthSessions.get(state);
|
|
231
|
-
// Poll target for the dialog: reports whether the loopback callback already delivered a code.
|
|
232
|
-
if (!session)
|
|
233
|
-
throw new GatewayError('invalid_request_error', 'Authorization session not found or expired', { status: 404 });
|
|
234
|
-
return { pending: true, callbackReceived: Boolean(session.code), providerId: session.providerId, expiresInMs: session.createdAt + OAUTH_TTL_MS - Date.now() };
|
|
235
|
-
});
|
|
236
|
-
/**
|
|
237
|
-
* Completes the flow from either the browser's loopback redirect or a pasted callback URL/code.
|
|
238
|
-
* The matching session is consumed, so a code can only be exchanged once.
|
|
239
|
-
*/
|
|
240
|
-
app.post('/api/admin/codex/oauth/complete', MutationOptions, async (req) => {
|
|
241
|
-
const body = z.object({ providerId: z.string().min(1), state: z.string().min(1).optional(), callbackUrl: z.string().min(1).max(8192) }).parse(req.body);
|
|
242
|
-
providerOrThrow(body.providerId);
|
|
243
|
-
const fallbackState = [...oauthSessions.entries()].filter(([, session]) => session.providerId === body.providerId).sort((a, b) => b[1].createdAt - a[1].createdAt)[0]?.[0];
|
|
244
|
-
const state = body.state ?? fallbackState;
|
|
245
|
-
const session = state ? oauthSessions.get(state) : undefined;
|
|
246
|
-
if (!state || !session || session.providerId !== body.providerId)
|
|
247
|
-
throw new GatewayError('invalid_request_error', 'Authorization session not found or expired', { status: 409 });
|
|
248
|
-
const code = extractCodeFromCallback(body.callbackUrl) ?? session.code;
|
|
249
|
-
if (!code)
|
|
250
|
-
throw new GatewayError('invalid_request_error', 'Could not find an authorization code in the pasted value', { status: 400 });
|
|
251
|
-
try {
|
|
252
|
-
const record = await exchangeCodexCode({ code, verifier: session.verifier });
|
|
253
|
-
oauthSessions.delete(state);
|
|
254
|
-
const result = upsertCodexAccount(body.providerId, record);
|
|
255
|
-
recordAudit({ action: 'codex.oauth.connect', success: true, targetType: 'codex_account', targetId: result.id, targetName: record.email ?? undefined, ip: req.ip, metadata: { status: result.status } });
|
|
256
|
-
return { account: toCodexAccountSummaryRow(accountById(result.id)), status: result.status };
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
const status = error.status ?? 502;
|
|
260
|
-
throw new GatewayError('authentication_error', redactString(error instanceof Error ? error.message : 'Codex authorization failed'), { status });
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
app.patch('/api/admin/codex/accounts/:id', MutationOptions, async (req) => {
|
|
264
|
-
const { id } = req.params;
|
|
265
|
-
const body = CodexAccountUpdate.parse(req.body);
|
|
266
|
-
accountById(id);
|
|
267
|
-
const raw = getRawDb();
|
|
268
|
-
const fields = [];
|
|
269
|
-
const values = [];
|
|
270
|
-
for (const [key, value] of Object.entries(body)) {
|
|
271
|
-
const column = { enabled: 'enabled', email: 'email', workspaceId: 'workspace_id', planType: 'plan_type', priority: 'priority', autostart: 'codex_autostart_enabled' }[key];
|
|
272
|
-
if (column) {
|
|
273
|
-
fields.push(`${column}=?`);
|
|
274
|
-
values.push(typeof value === 'boolean' ? (value ? 1 : 0) : value);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
fields.push('updated_at=?');
|
|
278
|
-
values.push(new Date().toISOString(), id);
|
|
279
|
-
raw.prepare(`UPDATE codex_accounts SET ${fields.join(',')} WHERE id=?`).run(...values);
|
|
280
|
-
recordAudit({ action: 'codex.accounts.update', success: true, targetType: 'codex_account', targetId: id, ip: req.ip, metadata: { fields: Object.keys(body) } });
|
|
281
|
-
return { account: toCodexAccountSummaryRow(accountById(id)) };
|
|
282
|
-
});
|
|
283
|
-
app.delete('/api/admin/codex/accounts/:id', MutationOptions, async (req) => {
|
|
284
|
-
const { id } = req.params;
|
|
285
|
-
const account = accountById(id);
|
|
286
|
-
// Hard delete: the encrypted credentials must not linger on disk. Historical request attempts
|
|
287
|
-
// survive because request_attempts.codex_account_id is ON DELETE SET NULL.
|
|
288
|
-
getRawDb().prepare('DELETE FROM codex_accounts WHERE id=?').run(id);
|
|
289
|
-
recordAudit({ action: 'codex.accounts.delete', success: true, targetType: 'codex_account', targetId: id, targetName: account.email ?? undefined, ip: req.ip });
|
|
290
|
-
return { ok: true, deleted: true };
|
|
291
|
-
});
|
|
292
|
-
app.post('/api/admin/codex/accounts/:id/test', MutationOptions, async (req) => {
|
|
293
|
-
const { id } = req.params;
|
|
294
|
-
const { provider, accountId } = accountTarget(id);
|
|
295
|
-
const result = await withCredentials(id, (credentials) => probeCodex({
|
|
296
|
-
baseUrl: provider.base_url, accountId: accountId ?? '', accessToken: credentials.accessToken,
|
|
297
|
-
accountRecordId: id, customHeaders: {}, totalTimeoutMs: Math.min(provider.total_timeout_ms, 20_000),
|
|
298
|
-
}));
|
|
299
|
-
setCodexAccountHealth(id, result.ok ? 'healthy' : 'down', result.ok ? null : redactString(result.detail));
|
|
300
|
-
recordAudit({ action: 'codex.accounts.test', success: result.ok, targetType: 'codex_account', targetId: id, ip: req.ip, metadata: { detail: redactString(result.detail) } });
|
|
301
|
-
return { ok: result.ok, detail: redactString(result.detail), latencyMs: result.latencyMs, modelCount: result.modelCount ?? null };
|
|
302
|
-
});
|
|
303
|
-
/** Refresh the 5h/weekly quota snapshot for one account. */
|
|
304
|
-
app.post('/api/admin/codex/accounts/:id/usage', MutationOptions, async (req) => {
|
|
305
|
-
const { id } = req.params;
|
|
306
|
-
const { provider, accountId } = accountTarget(id);
|
|
307
|
-
await refreshStoredCodexUsage(id, provider, { id, chatgpt_account_id: accountId });
|
|
308
|
-
return { account: toCodexAccountSummaryRow(accountById(id)) };
|
|
309
|
-
});
|
|
310
|
-
/** Weekly reset credits available on the ChatGPT account. */
|
|
311
|
-
app.get('/api/admin/codex/accounts/:id/reset-credits', async (req) => {
|
|
312
|
-
const { id } = req.params;
|
|
313
|
-
const { accountId } = accountTarget(id);
|
|
314
|
-
const result = await withCredentials(id, (credentials) => fetchCodexResetCredits(credentials.accessToken, accountId ?? undefined));
|
|
315
|
-
return result;
|
|
316
|
-
});
|
|
317
|
-
/** Spend one weekly reset credit to restart the 5h window immediately. */
|
|
318
|
-
app.post('/api/admin/codex/accounts/:id/reset-quota', MutationOptions, async (req) => {
|
|
319
|
-
const { id } = req.params;
|
|
320
|
-
const { provider, accountId } = accountTarget(id);
|
|
321
|
-
const result = await withCredentials(id, (credentials) => consumeCodexResetCredit(credentials.accessToken, accountId ?? undefined));
|
|
322
|
-
if (result.ok)
|
|
323
|
-
await refreshStoredCodexUsage(id, provider, { id, chatgpt_account_id: accountId });
|
|
324
|
-
recordAudit({ action: 'codex.accounts.reset_quota', success: result.ok, targetType: 'codex_account', targetId: id, ip: req.ip, metadata: { code: result.code, windowsReset: result.windowsReset } });
|
|
325
|
-
if (!result.ok) {
|
|
326
|
-
const status = result.noCredit ? 409 : 502;
|
|
327
|
-
throw new GatewayError('invalid_request_error', result.noCredit ? 'No Codex reset credits available' : redactString(result.message ?? 'Codex reset credit request failed'), { status });
|
|
328
|
-
}
|
|
329
|
-
return { ok: true, windowsReset: result.windowsReset, account: toCodexAccountSummaryRow(accountById(id)) };
|
|
330
|
-
});
|
|
331
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 240 10% 3.9%;--card: 0 0% 100%;--card-foreground: 240 10% 3.9%;--popover: 0 0% 100%;--popover-foreground: 240 10% 3.9%;--primary: 341 86% 41%;--primary-foreground: 0 0% 100%;--secondary: 240 4.8% 95.9%;--secondary-foreground: 240 5.9% 10%;--muted: 240 4.8% 95.9%;--muted-foreground: 240 3.8% 46.1%;--accent: 240 4.8% 95.9%;--accent-foreground: 240 5.9% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 240 5.9% 90%;--input: 240 5.9% 90%;--ring: 341 86% 41%;--sidebar: 240 5.9% 98%;--sidebar-foreground: 240 10% 3.9%;--radius: .5rem}.dark{--background: 240 10% 3.9%;--foreground: 0 0% 98%;--card: 240 10% 6%;--card-foreground: 0 0% 98%;--popover: 240 10% 5%;--popover-foreground: 0 0% 98%;--primary: 341 86% 55%;--primary-foreground: 0 0% 100%;--secondary: 240 3.7% 15.9%;--secondary-foreground: 0 0% 98%;--muted: 240 3.7% 15.9%;--muted-foreground: 240 5% 64.9%;--accent: 240 3.7% 15.9%;--accent-foreground: 0 0% 98%;--destructive: 0 72% 51%;--destructive-foreground: 0 0% 98%;--border: 240 3.7% 15.9%;--input: 240 3.7% 15.9%;--ring: 341 86% 55%;--sidebar: 240 10% 5%;--sidebar-foreground: 0 0% 98%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground));font-feature-settings:"rlig" 1,"calt" 1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-2\.5{left:.625rem}.left-\[50\%\]{left:50%}.right-2{right:.5rem}.right-4{right:1rem}.top-1\/2{top:50%}.top-16{top:4rem}.top-2\.5{top:.625rem}.top-4{top:1rem}.top-\[50\%\]{top:50%}.z-10{z-index:10}.z-50{z-index:50}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-1{margin-bottom:.25rem}.mb-3{margin-bottom:.75rem}.mb-6{margin-bottom:1.5rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.ml-2{margin-left:.5rem}.mr-0\.5{margin-right:.125rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-20{height:5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-60{max-height:15rem}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.max-h-\[calc\(100vh-8rem\)\]{max-height:calc(100vh - 8rem)}.min-h-32{min-height:8rem}.min-h-\[60px\]{min-height:60px}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-40{width:10rem}.w-56{width:14rem}.w-60{width:15rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[8rem\]{min-width:8rem}.min-w-\[9rem\]{min-width:9rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-64{max-width:16rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-amber-500\/40{border-color:#f59e0b66}.border-amber-600\/50{border-color:#d9770680}.border-border{border-color:hsl(var(--border))}.border-destructive{border-color:hsl(var(--destructive))}.border-destructive\/40{border-color:hsl(var(--destructive) / .4)}.border-input{border-color:hsl(var(--input))}.border-primary{border-color:hsl(var(--primary))}.border-transparent{border-color:transparent}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/90{background-color:#f59e0be6}.bg-background{background-color:hsl(var(--background))}.bg-black\/15{background-color:#00000026}.bg-black\/80{background-color:#000c}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-destructive\/10{background-color:hsl(var(--destructive) / .1)}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity, 1))}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity, 1))}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-muted{background-color:hsl(var(--muted))}.bg-muted\/30{background-color:hsl(var(--muted) / .3)}.bg-muted\/60{background-color:hsl(var(--muted) / .6)}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/10{background-color:hsl(var(--primary) / .1)}.bg-primary\/70{background-color:hsl(var(--primary) / .7)}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-sidebar{background-color:hsl(var(--sidebar))}.bg-transparent{background-color:transparent}.fill-amber-400{fill:#fbbf24}.fill-amber-500\/10{fill:#f59e0b1a}.fill-emerald-400{fill:#34d399}.fill-emerald-500{fill:#10b981}.fill-foreground{fill:hsl(var(--foreground))}.fill-muted-foreground{fill:hsl(var(--muted-foreground))}.fill-primary\/10{fill:hsl(var(--primary) / .1)}.fill-red-400{fill:#f87171}.fill-red-500{fill:#ef4444}.stroke-amber-500{stroke:#f59e0b}.stroke-emerald-500{stroke:#10b981}.stroke-primary{stroke:hsl(var(--primary))}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-6{padding-bottom:1.5rem}.pl-8{padding-left:2rem}.pr-0{padding-right:0}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-text-bottom{vertical-align:text-bottom}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-amber-950{--tw-text-opacity: 1;color:rgb(69 26 3 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-current{color:currentColor}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-500{--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-foreground{color:hsl(var(--foreground))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-muted-foreground\/60{color:hsl(var(--muted-foreground) / .6)}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sidebar-foreground{color:hsl(var(--sidebar-foreground))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}@keyframes route-active-pulse{0%{opacity:.35}50%{opacity:1}to{opacity:.35}}.route-active{animation:route-active-pulse 1.4s ease-in-out infinite}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:hsl(var(--border));border-radius:4px}::-webkit-scrollbar-track{background:transparent}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.last\:border-0:last-child{border-width:0px}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted:hover{background-color:hsl(var(--muted))}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-95:hover{opacity:.95}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color: hsl(var(--background))}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[state\=checked\]\:translate-x-4[data-state=checked]{--tw-translate-x: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:hsl(var(--muted))}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:hsl(var(--input))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:hsl(var(--primary-foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=active\]\:shadow[data-state=active]{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dark\:border-amber-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(146 64 14 / var(--tw-border-opacity, 1))}.dark\:bg-amber-950\/20:is(.dark *){background-color:#451a0333}.dark\:bg-emerald-900\/40:is(.dark *){background-color:#064e3b66}.dark\:bg-green-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.dark\:bg-red-900\/40:is(.dark *){background-color:#7f1d1d66}.dark\:text-amber-200:is(.dark *){--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.dark\:text-amber-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.dark\:text-emerald-300:is(.dark *){--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.dark\:text-green-200:is(.dark *){--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.dark\:text-red-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}@media(min-width:640px){.sm\:max-w-\[425px\]{max-width:425px}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media(min-width:768px){.md\:flex{display:flex}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-col{flex-direction:column}}@media(min-width:1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-width:0px}.\[\&_tr\]\:border-b tr{border-bottom-width:1px}
|