opencode-claude-code-ua 0.0.0-stage → 0.3.6
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/LICENSE +23 -0
- package/NOTICE.md +9 -0
- package/README.md +196 -2
- package/assets/icon.svg +9 -0
- package/index.mjs +23 -0
- package/lib/parameters.mjs +108 -0
- package/lib/ua-protocol.mjs +43 -0
- package/lib/ua-test-plan.mjs +232 -0
- package/package.json +80 -4
- package/setup.mjs +39 -0
- package/src/account-profile.mjs +73 -0
- package/src/accounts.mjs +281 -0
- package/src/connection.mjs +144 -0
- package/src/diagnostics.mjs +37 -0
- package/src/headers.mjs +60 -0
- package/src/newapi.mjs +237 -0
- package/src/options.mjs +48 -0
- package/src/plugin.mjs +374 -0
- package/src/profile.mjs +217 -0
- package/src/runtime.mjs +39 -0
- package/src/schedule.mjs +88 -0
- package/src/scheduler.mjs +141 -0
- package/src/setup-server.mjs +465 -0
- package/src/store.mjs +58 -0
- package/src/transport.mjs +84 -0
- package/src/ua-model-catalog.mjs +218 -0
- package/src/ua-plugin.mjs +207 -0
- package/src/ua-runtime.mjs +229 -0
- package/src/ua-service.mjs +97 -0
- package/src/ua-store.mjs +657 -0
- package/src/ua-tests.mjs +153 -0
- package/src/ua-transform.mjs +478 -0
- package/ui/accounts.css +75 -0
- package/ui/accounts.html +57 -0
- package/ui/accounts.js +582 -0
- package/ui/pro.css +1682 -0
- package/ui/pro.html +348 -0
- package/ui/pro.js +830 -0
- package/ui/theme.js +39 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { applyParameters, compileParameters, isObject } from '../lib/parameters.mjs';
|
|
3
|
+
|
|
4
|
+
// Pinned to sub2api-magpie-plugin a22d079b (0.4.16), not a live client version.
|
|
5
|
+
const CODEX_VERSION = '0.159.0';
|
|
6
|
+
const CLAUDE_VERSION = '2.1.296';
|
|
7
|
+
const IDENTITY_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
8
|
+
const FAST_BETA = 'fast-mode-2026-02-01';
|
|
9
|
+
const LIMITS = { maxBodyBytes: 32 << 20, bodyTimeoutMs: 15000, maxMetadataBytes: 1 << 20 };
|
|
10
|
+
const OPERATIONS = { anthropic: ['messages', 'count_tokens'], responses: ['responses'], 'chat-completions': ['chat/completions'], gemini: ['generateContent', 'streamGenerateContent', 'countTokens'] };
|
|
11
|
+
const TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
12
|
+
const BAD_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
13
|
+
const BUSINESS = new Set(['model', 'messages', 'stream', 'tools', 'tool_choice', 'input', 'contents', 'systemInstruction']);
|
|
14
|
+
const PROTECTED = new Set(['authorization', 'proxy-authorization', 'proxy-authenticate', 'www-authenticate', 'x-api-key', 'api-key', 'x-goog-api-key', 'cookie', 'set-cookie', 'host', 'content-length', 'connection', 'transfer-encoding', 'upgrade', 'keep-alive', 'te', 'trailer', 'proxy-connection', 'accept-encoding', 'x-openai-actor-authorization', 'x-anthropic-billing-header', 'x-sub2api-magpie-plugin-speed-model']);
|
|
15
|
+
const VARIABLES = new Set(['session.id', 'request.id', 'client.version', 'model.id', 'reasoning.effort', 'timestamp', 'platform', 'arch', 'terminal']);
|
|
16
|
+
const SESSION_HEADERS = ['session-id', 'session_id', 'x-session-id', 'x-claude-code-session-id', 'x-opencode-session'];
|
|
17
|
+
const CODEX_FAMILY = /^(?:codex_cli_rs|codex-tui|codex_vscode_copilot|codex_vscode|codex_app|codex_chatgpt_desktop|codex_atlas|codex_exec|codex_sdk_ts)$/;
|
|
18
|
+
const VS_BETA = 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,per-turn-control-2026-07-01,mid-conversation-tool-changes-2026-07-01,advanced-tool-use-2025-11-20,effort-2025-11-24,dangerous-tool-use-2026-09-03,afk-mode-2026-01-31';
|
|
19
|
+
|
|
20
|
+
function error(path, message = 'UA PRO configuration is invalid', status = 400) {
|
|
21
|
+
const e = new Error(message); e.status = status; e.fields = { [path]: message }; return e;
|
|
22
|
+
}
|
|
23
|
+
function known(value, keys, path) {
|
|
24
|
+
if (!isObject(value) || Object.keys(value).some(k => !keys.includes(k))) throw error(path);
|
|
25
|
+
}
|
|
26
|
+
function enumeration(value, values, path) {
|
|
27
|
+
if (!values.includes(value)) throw error(path); return value;
|
|
28
|
+
}
|
|
29
|
+
function text(value, path, max = 16384, blank = true) {
|
|
30
|
+
if (typeof value !== 'string' || value.length > max || /[\u0000-\u001f\u007f]/u.test(value) || (!blank && !value.trim())) throw error(path);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function protectedHeader(name) { return PROTECTED.has(name) || name.startsWith('x-magpie-'); }
|
|
34
|
+
function billingKey(name) { return /^(?:billing(?:[_-].*)?|x[_-]anthropic[_-]billing[_-]header)$/i.test(name); }
|
|
35
|
+
const billingBlock = value => typeof value === 'string' && /^\s*x-anthropic-billing-header\s*:/i.test(value);
|
|
36
|
+
const optional = (value, fallback) => value === undefined ? fallback : value;
|
|
37
|
+
function billingSnapshot(payload) {
|
|
38
|
+
const records = []; const pending = [[payload, 'body']];
|
|
39
|
+
while (pending.length) {
|
|
40
|
+
const [value, path] = pending.pop();
|
|
41
|
+
if (!value || typeof value !== 'object') continue;
|
|
42
|
+
for (const [name, child] of Object.entries(value)) {
|
|
43
|
+
const next = `${path}.${Array.isArray(value) ? '*' : name}`;
|
|
44
|
+
if (billingKey(name)) records.push([next, JSON.stringify(child)]);
|
|
45
|
+
else if (child !== null && typeof child === 'object') pending.push([child, next]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Only actual existing system billing blocks are identity-protected. Prompt
|
|
49
|
+
// prose mentioning the header name remains ordinary user configuration.
|
|
50
|
+
const system = payload.system;
|
|
51
|
+
if (billingBlock(system)) records.push(['body.system.billing', JSON.stringify({ type: 'text', text: system })]);
|
|
52
|
+
if (Array.isArray(system)) for (const block of system) {
|
|
53
|
+
if (block?.type === 'text' && billingBlock(block.text)) records.push(['body.system.billing', JSON.stringify(block)]);
|
|
54
|
+
}
|
|
55
|
+
return JSON.stringify(records.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))));
|
|
56
|
+
}
|
|
57
|
+
function headerText(value, path) {
|
|
58
|
+
text(value, path);
|
|
59
|
+
if (/[^\u0020-\u00ff]/u.test(value)) throw error(path);
|
|
60
|
+
let cursor = 0;
|
|
61
|
+
while (cursor < value.length) {
|
|
62
|
+
const open = value.indexOf('{{', cursor); const close = value.indexOf('}}', cursor);
|
|
63
|
+
if (open < 0 && close < 0) break;
|
|
64
|
+
if (open < 0 || (close >= 0 && close < open)) throw error(path);
|
|
65
|
+
const end = value.indexOf('}}', open + 2);
|
|
66
|
+
if (end < 0 || !VARIABLES.has(value.slice(open + 2, end))) throw error(path);
|
|
67
|
+
cursor = end + 2;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function json(value, path, depth = 0) {
|
|
72
|
+
if (depth > 32) throw error(path);
|
|
73
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
74
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
75
|
+
if (Array.isArray(value)) return value.map(v => json(v, path, depth + 1));
|
|
76
|
+
if (!isObject(value)) throw error(path);
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const [key, val] of Object.entries(value)) {
|
|
79
|
+
if (BAD_KEYS.has(key) || billingKey(key)) throw error(path);
|
|
80
|
+
out[key] = json(val, path, depth + 1);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function parameters(raw = {}) {
|
|
85
|
+
known(raw, ['defaults', 'overrides', 'remove'], 'body.parameters');
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const section of ['defaults', 'overrides']) {
|
|
88
|
+
const values = optional(raw[section], {});
|
|
89
|
+
if (!isObject(values) || Object.keys(values).some(k => BUSINESS.has(k))) throw error(`body.parameters.${section}`);
|
|
90
|
+
result[section] = json(values, `body.parameters.${section}`);
|
|
91
|
+
}
|
|
92
|
+
const remove = optional(raw.remove, []);
|
|
93
|
+
if (!Array.isArray(remove) || remove.length > 256) throw error('body.parameters.remove');
|
|
94
|
+
result.remove = remove.map(path => {
|
|
95
|
+
if (typeof path !== 'string' || path.length > 512 || !/^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)*$/.test(path)) throw error('body.parameters.remove');
|
|
96
|
+
const keys = path.split('.');
|
|
97
|
+
if (BUSINESS.has(keys[0]) || keys.some(k => BAD_KEYS.has(k) || billingKey(k))) throw error('body.parameters.remove');
|
|
98
|
+
return path;
|
|
99
|
+
});
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function validateTemplate(raw) {
|
|
104
|
+
known(raw, ['schemaVersion', 'id', 'name', 'revision', 'kind', 'basedOn', 'protocols', 'operations', 'identity', 'headers', 'body', 'limits', 'archived'], 'template');
|
|
105
|
+
if (raw.schemaVersion !== 1) throw error('schemaVersion');
|
|
106
|
+
if (!Number.isSafeInteger(raw.revision) || raw.revision < 1) throw error('revision');
|
|
107
|
+
const out = { schemaVersion: 1, id: text(raw.id, 'id', 160, false), name: text(raw.name, 'name', 160, false), revision: raw.revision, kind: enumeration(raw.kind, ['builtin', 'custom'], 'kind') };
|
|
108
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(out.id)) throw error('id');
|
|
109
|
+
if (raw.archived !== undefined) {
|
|
110
|
+
if (typeof raw.archived !== 'boolean') throw error('archived');
|
|
111
|
+
out.archived = raw.archived;
|
|
112
|
+
}
|
|
113
|
+
if (raw.basedOn !== undefined) {
|
|
114
|
+
known(raw.basedOn, ['id', 'revision'], 'basedOn');
|
|
115
|
+
if (!Number.isSafeInteger(raw.basedOn.revision) || raw.basedOn.revision < 1) throw error('basedOn.revision');
|
|
116
|
+
out.basedOn = { id: text(raw.basedOn.id, 'basedOn.id', 160, false), revision: raw.basedOn.revision };
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(raw.protocols) || !raw.protocols.length || raw.protocols.length > 4) throw error('protocols');
|
|
119
|
+
out.protocols = [...new Set(raw.protocols.map(p => enumeration(p, Object.keys(OPERATIONS), 'protocols')))];
|
|
120
|
+
if (!Array.isArray(raw.operations) || !raw.operations.length || raw.operations.length > 7) throw error('operations');
|
|
121
|
+
out.operations = [...new Set(raw.operations.map(o => enumeration(o, out.protocols.flatMap(p => OPERATIONS[p]), 'operations')))];
|
|
122
|
+
const identity = optional(raw.identity, {});
|
|
123
|
+
known(identity, ['detector', 'mode', 'session', 'userAgent', 'version'], 'identity');
|
|
124
|
+
out.identity = {
|
|
125
|
+
detector: enumeration(optional(identity.detector, 'none'), ['codex-ua', 'claude-ua', 'none'], 'identity.detector'),
|
|
126
|
+
mode: enumeration(optional(identity.mode, 'preserve'), ['reference-compatible', 'preserve'], 'identity.mode'),
|
|
127
|
+
session: enumeration(optional(identity.session, 'preserve'), ['reference-stable', 'host', 'preserve', 'off'], 'identity.session'),
|
|
128
|
+
userAgent: headerText(optional(identity.userAgent, ''), 'identity.userAgent'), version: headerText(optional(identity.version, ''), 'identity.version'),
|
|
129
|
+
};
|
|
130
|
+
if (!Array.isArray(raw.headers) || raw.headers.length > 128) throw error('headers');
|
|
131
|
+
let headerBytes = 0;
|
|
132
|
+
out.headers = raw.headers.map((h, i) => {
|
|
133
|
+
const path = `headers[${i}]`;
|
|
134
|
+
known(h, ['name', 'action', 'value', 'enabled', 'when', 'operations'], path);
|
|
135
|
+
if (typeof h.name !== 'string' || h.name.length > 256 || !TOKEN.test(h.name)) throw error(`${path}.name`);
|
|
136
|
+
const name = h.name.toLowerCase();
|
|
137
|
+
if (protectedHeader(name) || name === 'content-encoding') throw error(`${path}.name`);
|
|
138
|
+
const action = enumeration(h.action, ['set', 'set-if-missing', 'set-if-empty', 'delete', 'preserve'], `${path}.action`);
|
|
139
|
+
if (h.enabled !== undefined && typeof h.enabled !== 'boolean') throw error(`${path}.enabled`);
|
|
140
|
+
const entry = { name, action, enabled: optional(h.enabled, true), when: enumeration(optional(h.when, 'always'), ['always', 'native', 'third-party'], `${path}.when`) };
|
|
141
|
+
if (h.operations !== undefined) {
|
|
142
|
+
if (!Array.isArray(h.operations) || !h.operations.length || h.operations.length > 7) throw error(`${path}.operations`);
|
|
143
|
+
entry.operations = [...new Set(h.operations.map(o => enumeration(o, out.operations, `${path}.operations`)))];
|
|
144
|
+
}
|
|
145
|
+
if (h.value !== undefined) entry.value = h.value === null ? null : headerText(h.value, `${path}.value`);
|
|
146
|
+
if (!['delete', 'preserve'].includes(action) && h.value === undefined) throw error(`${path}.value`);
|
|
147
|
+
headerBytes += name.length + (entry.value?.length ?? 0) + 4;
|
|
148
|
+
return entry;
|
|
149
|
+
});
|
|
150
|
+
if (headerBytes > 65536) throw error('headers');
|
|
151
|
+
const body = optional(raw.body, {});
|
|
152
|
+
known(body, ['codexMetadata', 'claudeCompatibility', 'identityprompt', 'fastBeta', 'parameters'], 'body');
|
|
153
|
+
if (body.codexMetadata !== undefined && typeof body.codexMetadata !== 'boolean') throw error('body.codexMetadata');
|
|
154
|
+
if (body.fastBeta !== undefined && typeof body.fastBeta !== 'boolean') throw error('body.fastBeta');
|
|
155
|
+
out.body = { codexMetadata: optional(body.codexMetadata, false), claudeCompatibility: enumeration(optional(body.claudeCompatibility, 'off'), ['third-party-messages', 'off'], 'body.claudeCompatibility'), fastBeta: optional(body.fastBeta, false), parameters: parameters(body.parameters) };
|
|
156
|
+
if (body.identityprompt !== undefined) {
|
|
157
|
+
if (typeof body.identityprompt !== 'string' || !body.identityprompt.trim() || body.identityprompt.length > 8192 || body.identityprompt.includes('\0')) throw error('body.identityprompt');
|
|
158
|
+
out.body.identityprompt = body.identityprompt;
|
|
159
|
+
}
|
|
160
|
+
// Reuse Messages' established compile-time parameter restrictions.
|
|
161
|
+
if (out.protocols.includes('anthropic')) {
|
|
162
|
+
try { compileParameters(out.body.parameters); } catch { throw error('body.parameters', 'Messages parameter rules are invalid'); }
|
|
163
|
+
}
|
|
164
|
+
const limits = optional(raw.limits, {});
|
|
165
|
+
known(limits, Object.keys(LIMITS), 'limits'); out.limits = {};
|
|
166
|
+
for (const key of Object.keys(LIMITS)) {
|
|
167
|
+
const max = key === 'maxBodyBytes' ? 64 << 20 : key === 'maxMetadataBytes' ? 4 << 20 : 60000;
|
|
168
|
+
const value = optional(limits[key], LIMITS[key]);
|
|
169
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max) throw error(`limits.${key}`);
|
|
170
|
+
out.limits[key] = value;
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function header(name, action, value, when = 'always') { return { name, action, ...(value === undefined ? {} : { value }), enabled: true, when }; }
|
|
176
|
+
export function builtinTemplates() {
|
|
177
|
+
const make = (id, name, protocol, detector, version, userAgent) => ({
|
|
178
|
+
schemaVersion: 1, id, name, revision: 1, kind: 'builtin', protocols: [protocol], operations: [...OPERATIONS[protocol]],
|
|
179
|
+
identity: { detector, mode: 'reference-compatible', session: 'reference-stable', userAgent, version }, headers: [],
|
|
180
|
+
body: { codexMetadata: false, claudeCompatibility: 'off', fastBeta: false, parameters: { defaults: {}, overrides: {}, remove: [] } }, limits: { ...LIMITS },
|
|
181
|
+
});
|
|
182
|
+
const codex = make('builtin.codex', 'Codex', 'responses', 'codex-ua', CODEX_VERSION, 'codex_cli_rs/{{client.version}} ({{platform}}; {{arch}}) {{terminal}}');
|
|
183
|
+
codex.headers = [header('openai-beta', 'set-if-missing', 'responses=experimental'), header('originator', 'set-if-missing', 'codex_cli_rs'), header('version', 'set-if-missing', '{{client.version}}')];
|
|
184
|
+
codex.body.codexMetadata = true;
|
|
185
|
+
const claude = make('builtin.claude-code', 'Claude Code', 'anthropic', 'claude-ua', CLAUDE_VERSION, `claude-cli/${CLAUDE_VERSION} (external, cli)`);
|
|
186
|
+
claude.headers = [header('x-app', 'set-if-missing', 'cli'), header('anthropic-version', 'set-if-missing', '2023-06-01')];
|
|
187
|
+
// These empty-value rules apply to third-party Messages inference only.
|
|
188
|
+
claude.headers.push(...[header('x-app', 'set-if-empty', 'cli', 'third-party'), header('anthropic-version', 'set-if-empty', '2023-06-01', 'third-party'), header('anthropic-beta', 'set-if-empty', 'claude-code-20250219', 'third-party')].map(h => ({ ...h, operations: ['messages'] })));
|
|
189
|
+
claude.body.claudeCompatibility = 'third-party-messages'; claude.body.fastBeta = true; claude.body.identityprompt = IDENTITY_PROMPT;
|
|
190
|
+
const custom = make('custom.claude-vscode-2316', 'Claude Code VS Code 2.1.316', 'anthropic', 'claude-ua', '2.1.316', 'claude-cli/2.1.316 (external, claude-vscode, agent-sdk/0.3.316)');
|
|
191
|
+
custom.kind = 'custom'; custom.basedOn = { id: claude.id, revision: 1 }; custom.identity.mode = 'preserve'; custom.identity.session = 'preserve';
|
|
192
|
+
custom.headers = Object.entries({ accept: 'application/json', 'anthropic-beta': VS_BETA, 'anthropic-dangerous-direct-browser-access': 'true', 'anthropic-version': '2023-06-01', 'user-agent': custom.identity.userAgent, 'x-app': 'cli', 'x-stainless-arch': 'x64', 'x-stainless-lang': 'js', 'x-stainless-os': 'Windows', 'x-stainless-package-version': '0.128.0', 'x-stainless-retry-count': '0', 'x-stainless-runtime': 'node', 'x-stainless-runtime-version': 'v26.12.1', 'x-stainless-timeout': '300' }).map(([n, v]) => header(n, 'set', v));
|
|
193
|
+
return [codex, claude, custom];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const hash = (...parts) => createHash('sha256').update(JSON.stringify(parts)).digest('hex');
|
|
197
|
+
function uuid(...parts) {
|
|
198
|
+
const h = hash('codex-identity', ...parts).slice(0, 32);
|
|
199
|
+
const id = h.slice(0, 12) + '4' + h.slice(13, 16) + (8 + (parseInt(h[16], 16) & 3)).toString(16) + h.slice(17);
|
|
200
|
+
return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
|
|
201
|
+
}
|
|
202
|
+
function nativeCaller(detector, headers) {
|
|
203
|
+
const ua = (headers.get('user-agent') ?? '').trim().toLowerCase();
|
|
204
|
+
if (detector === 'claude-ua') return /^claude-cli\/\d+\.\d+\.\d+/.test(ua);
|
|
205
|
+
if (detector !== 'codex-ua') return false;
|
|
206
|
+
const slash = ua.indexOf('/');
|
|
207
|
+
if (slash < 0 || !/^\d+\.\d+\.\d+/.test(ua.slice(slash + 1).split(/[ (]/)[0])) return false;
|
|
208
|
+
if (CODEX_FAMILY.test(ua.slice(0, slash)) || ua.startsWith('codex ')) return true;
|
|
209
|
+
const open = ua.lastIndexOf('('); const close = ua.indexOf(')', open + 1);
|
|
210
|
+
const trailer = open >= 0 && close >= 0 ? ua.slice(open + 1, close).split(';')[0].trim() : '';
|
|
211
|
+
return CODEX_FAMILY.test(trailer) || trailer.startsWith('codex ');
|
|
212
|
+
}
|
|
213
|
+
function signalAbort(signal) {
|
|
214
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException('Request aborted', 'AbortError');
|
|
215
|
+
}
|
|
216
|
+
// Race each asynchronous read, including Blob.text. Cancel clone readers without
|
|
217
|
+
// awaiting tee cancellation: it may wait for the untouched original Request.
|
|
218
|
+
async function readBody(input, init, max, timeoutMs, signal) {
|
|
219
|
+
signalAbort(signal);
|
|
220
|
+
const source = init.body ?? (input instanceof Request ? input.body : undefined);
|
|
221
|
+
if (source === undefined || source === null) return { reason: 'absent' };
|
|
222
|
+
const failSize = () => ({ reason: 'size' });
|
|
223
|
+
if (typeof source === 'string') return Buffer.byteLength(source) <= max ? { text: source } : failSize();
|
|
224
|
+
if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {
|
|
225
|
+
if (source.byteLength > max) return failSize();
|
|
226
|
+
const bytes = ArrayBuffer.isView(source) ? new Uint8Array(source.buffer, source.byteOffset, source.byteLength) : new Uint8Array(source);
|
|
227
|
+
return { text: new TextDecoder().decode(bytes) };
|
|
228
|
+
}
|
|
229
|
+
const blob = source instanceof Blob;
|
|
230
|
+
if (blob && source.size > max) return failSize();
|
|
231
|
+
if (!blob && (init.body != null || !(input instanceof Request) || input.bodyUsed || !input.body)) return { reason: 'stream' };
|
|
232
|
+
let reader; let timer; let onAbort;
|
|
233
|
+
const stop = new Promise((resolve, reject) => {
|
|
234
|
+
timer = setTimeout(() => resolve({ reason: 'timeout' }), timeoutMs);
|
|
235
|
+
onAbort = () => reject(signal.reason ?? new DOMException('Request aborted', 'AbortError'));
|
|
236
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
237
|
+
});
|
|
238
|
+
const work = async () => {
|
|
239
|
+
if (blob) {
|
|
240
|
+
const value = await source.text();
|
|
241
|
+
return typeof value === 'string' && Buffer.byteLength(value) <= max ? { text: value } : failSize();
|
|
242
|
+
}
|
|
243
|
+
reader = input.clone().body.getReader();
|
|
244
|
+
const chunks = []; let size = 0;
|
|
245
|
+
while (true) {
|
|
246
|
+
const { done, value } = await reader.read();
|
|
247
|
+
signalAbort(signal);
|
|
248
|
+
if (done) return { text: Buffer.concat(chunks, size).toString('utf8') };
|
|
249
|
+
size += value.byteLength;
|
|
250
|
+
if (size > max) return failSize();
|
|
251
|
+
chunks.push(Buffer.from(value));
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
try { return await Promise.race([work(), stop]); }
|
|
255
|
+
catch (e) { signalAbort(signal); return { reason: 'unreadable' }; }
|
|
256
|
+
finally {
|
|
257
|
+
clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
|
|
258
|
+
if (reader) {
|
|
259
|
+
try { void reader.cancel().catch(() => {}); reader.releaseLock(); } catch { /* already closed */ }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function validUserID(value) {
|
|
264
|
+
if (typeof value !== 'string' || !value.trim()) return false;
|
|
265
|
+
const raw = value.trim();
|
|
266
|
+
if (raw[0] !== '{') return /^user_([a-fA-F0-9]{64})_account_([a-fA-F0-9-]*)_session_([a-fA-F0-9-]{36})$/.test(raw);
|
|
267
|
+
let parsed; try { parsed = JSON.parse(raw); } catch { return false; }
|
|
268
|
+
if (!isObject(parsed)) return false;
|
|
269
|
+
const fields = Object.create(null);
|
|
270
|
+
for (const [name, field] of Object.entries(parsed)) {
|
|
271
|
+
const key = name.toLowerCase();
|
|
272
|
+
if (!['device_id', 'account_uuid', 'session_id'].includes(key) || field === null) continue;
|
|
273
|
+
if (typeof field !== 'string') return false;
|
|
274
|
+
fields[key] = field;
|
|
275
|
+
}
|
|
276
|
+
return Boolean(fields.device_id && fields.session_id);
|
|
277
|
+
}
|
|
278
|
+
function systemBlocks(system, prompt) {
|
|
279
|
+
if (system === undefined || system === null || system === '') return [{ type: 'text', text: prompt }];
|
|
280
|
+
const matches = value => typeof value === 'string' && value.length <= Math.max(512, prompt.length * 2) && value.split(/\s+/).join(' ').trim() === prompt.split(/\s+/).join(' ').trim();
|
|
281
|
+
if (typeof system === 'string') return matches(system) ? [{ type: 'text', text: system }] : [{ type: 'text', text: prompt }, { type: 'text', text: system }];
|
|
282
|
+
if (!Array.isArray(system)) throw error('body.system', 'Messages system must be a string or block array');
|
|
283
|
+
return system.some(b => matches(b?.text)) ? undefined : [{ type: 'text', text: prompt }, ...system];
|
|
284
|
+
}
|
|
285
|
+
function mergeJSON(target, source, defaults) {
|
|
286
|
+
for (const [key, value] of Object.entries(source)) {
|
|
287
|
+
if (!Object.hasOwn(target, key)) target[key] = structuredClone(value);
|
|
288
|
+
else if (isObject(value) && isObject(target[key])) mergeJSON(target[key], value, defaults);
|
|
289
|
+
else if (!defaults) target[key] = structuredClone(value);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function applyGeneric(payload, plan) {
|
|
293
|
+
const result = structuredClone(payload);
|
|
294
|
+
mergeJSON(result, plan.defaults, true); mergeJSON(result, plan.overrides, false);
|
|
295
|
+
for (const path of plan.remove) {
|
|
296
|
+
const keys = path.split('.'); let parent = result;
|
|
297
|
+
for (const key of keys.slice(0, -1)) { parent = isObject(parent) && Object.hasOwn(parent, key) ? parent[key] : undefined; }
|
|
298
|
+
if (isObject(parent)) delete parent[keys.at(-1)];
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
function render(value, variables) {
|
|
303
|
+
let missing = false;
|
|
304
|
+
const output = value.replace(/\{\{([^{}]+)\}\}/g, (_, key) => {
|
|
305
|
+
const replacement = variables[key];
|
|
306
|
+
if (replacement === undefined || replacement === null || replacement === '') { missing = true; return ''; }
|
|
307
|
+
return String(replacement);
|
|
308
|
+
});
|
|
309
|
+
if (missing) return undefined;
|
|
310
|
+
// Substituted context is literal HTTP text, not another template to compile.
|
|
311
|
+
try {
|
|
312
|
+
text(output, 'context');
|
|
313
|
+
if (/[^\u0020-\u00ff]/u.test(output)) throw error('context');
|
|
314
|
+
return output;
|
|
315
|
+
} catch { throw error('context', 'Request context cannot be used in an HTTP header'); }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function transformRequest({ template, protocol, operation, input, init = {}, context = {} }) {
|
|
319
|
+
if (!Object.hasOwn(OPERATIONS, protocol) || !OPERATIONS[protocol].includes(operation)) throw error('operation', 'Protocol and operation do not match');
|
|
320
|
+
const t = template == null ? null : validateTemplate(template);
|
|
321
|
+
if (t && (!t.protocols.includes(protocol) || !t.operations.includes(operation))) throw error('template', 'Template does not match protocol and operation');
|
|
322
|
+
const original = new Headers(input instanceof Request ? input.headers : undefined);
|
|
323
|
+
new Headers(init.headers).forEach((value, name) => original.set(name, value));
|
|
324
|
+
const headers = new Headers(original);
|
|
325
|
+
const ua = (original.get('user-agent') ?? '').trim();
|
|
326
|
+
const visibleUA = Boolean(ua && !/^magpie(?:[/-]|$)/i.test(ua));
|
|
327
|
+
const native = t ? nativeCaller(t.identity.detector, original) : false;
|
|
328
|
+
const sessionHeader = SESSION_HEADERS.map(n => original.get(n)?.trim()).find(Boolean);
|
|
329
|
+
const hostSession = typeof context.sessionID === 'string' && context.sessionID.trim() ? context.sessionID : undefined;
|
|
330
|
+
const summary = { templateId: t?.id ?? null, revision: t?.revision ?? null, callerIdentity: native ? 'native' : visibleUA ? 'third-party' : 'unknown', sourceAvailability: { userAgent: visibleUA ? 'available' : 'unknown', session: hostSession ? 'host' : sessionHeader ? 'inbound' : 'unknown', credentialScope: context.credentialScope ? 'available' : 'unknown' }, changes: [] };
|
|
331
|
+
const change = (path, rule, action) => summary.changes.push({ path, rule, action });
|
|
332
|
+
const set = (name, value, rule) => {
|
|
333
|
+
if (headers.get(name) !== value) { headers.set(name, value); change(`headers.${name}`, rule, 'set'); }
|
|
334
|
+
};
|
|
335
|
+
const del = (name, rule) => { if (headers.has(name)) { headers.delete(name); change(`headers.${name}`, rule, 'delete'); } };
|
|
336
|
+
const signal = init.signal ?? (input instanceof Request ? input.signal : undefined);
|
|
337
|
+
signalAbort(signal);
|
|
338
|
+
let resultBody; let rewritten = false;
|
|
339
|
+
if (t) {
|
|
340
|
+
const mode = t.identity.mode === 'reference-compatible';
|
|
341
|
+
const sessionMode = t.identity.session;
|
|
342
|
+
const messages = protocol === 'anthropic' && operation === 'messages';
|
|
343
|
+
const complete = messages && !native && t.body.claudeCompatibility === 'third-party-messages';
|
|
344
|
+
const plan = t.body.parameters;
|
|
345
|
+
const active = !['count_tokens', 'countTokens'].includes(operation) && (Object.keys(plan.defaults).length || Object.keys(plan.overrides).length || plan.remove.length);
|
|
346
|
+
const method = (init.method ?? (input instanceof Request ? input.method : 'POST')).toUpperCase();
|
|
347
|
+
const hasBody = init.body != null || (input instanceof Request && input.body !== null);
|
|
348
|
+
const canBody = !['GET', 'HEAD'].includes(method) && hasBody;
|
|
349
|
+
const required = canBody && (complete || active);
|
|
350
|
+
const encoded = headers.has('content-encoding') && headers.get('content-encoding').toLowerCase().trim() !== 'identity';
|
|
351
|
+
if (required && encoded) throw error('body', 'Request body must be uncompressed for body transformation');
|
|
352
|
+
let payload;
|
|
353
|
+
const needsBodyVariables = t.headers.some(h => h.enabled && typeof h.value === 'string' && (h.value.includes('{{model.id}}') || h.value.includes('{{reasoning.effort}}')));
|
|
354
|
+
const inspect = required || (canBody && ((t.body.codexMetadata && protocol === 'responses') || (messages && t.body.fastBeta) || needsBodyVariables));
|
|
355
|
+
if (inspect && !encoded) {
|
|
356
|
+
const read = await readBody(input, init, required ? t.limits.maxBodyBytes : t.limits.maxMetadataBytes, t.limits.bodyTimeoutMs, signal);
|
|
357
|
+
signalAbort(signal);
|
|
358
|
+
if (read.text !== undefined) {
|
|
359
|
+
try { payload = JSON.parse(read.text); } catch { /* checked below */ }
|
|
360
|
+
if (!isObject(payload)) payload = undefined;
|
|
361
|
+
}
|
|
362
|
+
if (required && !payload) {
|
|
363
|
+
const reasons = { size: 'Request body exceeds the byte limit', timeout: 'Request body read timeout', stream: 'Streaming upload body cannot be transformed', unreadable: 'Request body is unreadable', absent: 'Request body is required' };
|
|
364
|
+
throw error('body', reasons[read.reason] ?? 'Request body must be a JSON object');
|
|
365
|
+
}
|
|
366
|
+
if (!payload) change('body', `inspection-${read.reason ?? 'invalid-json'}`, 'skip');
|
|
367
|
+
}
|
|
368
|
+
const protectedBilling = payload && required ? billingSnapshot(payload) : undefined;
|
|
369
|
+
const bodyModel = typeof payload?.model === 'string' && payload.model.trim() ? payload.model.trim() : undefined;
|
|
370
|
+
const scope = typeof context.credentialScope === 'string' ? context.credentialScope : '';
|
|
371
|
+
const base = typeof context.baseURL === 'string' ? context.baseURL : '';
|
|
372
|
+
if (t.body.codexMetadata && protocol === 'responses' && payload) {
|
|
373
|
+
let size = 0;
|
|
374
|
+
const promote = (name, value) => {
|
|
375
|
+
const lower = name.toLowerCase();
|
|
376
|
+
if (headers.has(lower) || headers.has(lower.replace(/-/g, '_')) || protectedHeader(lower) || typeof value !== 'string' || !value.trim() || size + name.length + value.length + 4 > 8192) return;
|
|
377
|
+
try { headerText(value, 'metadata'); set(lower, value, 'codex-metadata'); size += name.length + value.length + 4; } catch { /* untrusted metadata is optional */ }
|
|
378
|
+
};
|
|
379
|
+
if (isObject(payload.client_metadata)) for (const [field, value] of Object.entries(payload.client_metadata)) {
|
|
380
|
+
const name = field === 'session_id' ? 'session-id' : field === 'thread_id' ? 'thread-id' : field;
|
|
381
|
+
if (['session-id', 'thread-id'].includes(name) || /^x-codex-[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i.test(name)) promote(name, value);
|
|
382
|
+
}
|
|
383
|
+
promote('session-id', payload.prompt_cache_key); promote('thread-id', payload.prompt_cache_key);
|
|
384
|
+
}
|
|
385
|
+
let version = t.identity.version;
|
|
386
|
+
if (t.identity.detector === 'codex-ua') {
|
|
387
|
+
const given = (original.get('version') ?? '').trim();
|
|
388
|
+
const fromUA = /^(?:codex[^/\s]*|acp-extension-codex|Codex Desktop)\/(\d+(?:\.\d+){1,3})(?=\s|$)/i.exec(ua)?.[1];
|
|
389
|
+
version = /^\d+(?:\.\d+){1,3}$/.test(given) ? given : fromUA ?? version;
|
|
390
|
+
}
|
|
391
|
+
const platform = context.platform ?? process.platform;
|
|
392
|
+
const variables = { 'session.id': hostSession, 'request.id': context.requestId, 'client.version': version, 'model.id': context.model ?? bodyModel, 'reasoning.effort': payload?.output_config?.effort, timestamp: typeof context.now === 'function' ? context.now() : context.now ?? Date.now(), platform: platform === 'darwin' ? 'Mac OS' : platform === 'win32' ? 'Windows' : 'Linux', arch: (context.arch ?? process.arch) === 'x64' ? 'x86_64' : context.arch ?? process.arch, terminal: platform === 'darwin' ? 'Apple_Terminal/455' : platform === 'win32' ? 'WindowsTerminal' : 'xterm-256color' };
|
|
393
|
+
if (mode && !native && t.identity.userAgent) {
|
|
394
|
+
const value = render(t.identity.userAgent, variables);
|
|
395
|
+
if (value !== undefined) set('user-agent', value, 'identity-user-agent');
|
|
396
|
+
}
|
|
397
|
+
if (complete && canBody && payload) {
|
|
398
|
+
const system = systemBlocks(payload.system, t.body.identityprompt ?? IDENTITY_PROMPT);
|
|
399
|
+
const metadata = isObject(payload.metadata) ? payload.metadata : {};
|
|
400
|
+
const valid = validUserID(metadata.user_id);
|
|
401
|
+
if (system !== undefined || !valid) {
|
|
402
|
+
payload = { ...payload, ...(system === undefined ? {} : { system }) };
|
|
403
|
+
if (system !== undefined) change('body.system', 'claude-compatibility', 'complete');
|
|
404
|
+
if (!valid) {
|
|
405
|
+
const session = sessionMode === 'off' ? '' : hostSession ?? sessionHeader ?? '';
|
|
406
|
+
if (!hostSession && !sessionHeader) change('context.session', 'claude-session-fallback-unknown', 'derive');
|
|
407
|
+
payload.metadata = { ...metadata, user_id: JSON.stringify({ device_id: hash('claude-device', base, scope), account_uuid: uuid(base, scope, 'claude-account'), session_id: uuid(base, scope, session, 'claude-session') }) };
|
|
408
|
+
change('body.metadata.user_id', 'claude-compatibility', 'complete');
|
|
409
|
+
}
|
|
410
|
+
rewritten = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (active && canBody && payload) {
|
|
414
|
+
try {
|
|
415
|
+
const next = protocol === 'anthropic' && messages ? applyParameters(payload, compileParameters(plan)) : applyGeneric(payload, plan);
|
|
416
|
+
if (JSON.stringify(next) !== JSON.stringify(payload)) { payload = next; rewritten = true; change('body', 'explicit-parameters', 'merge'); }
|
|
417
|
+
} catch { throw error('body.parameters', 'Request parameter transformation is invalid'); }
|
|
418
|
+
}
|
|
419
|
+
if (messages && mode && !native && ['reference-stable', 'host'].includes(sessionMode) && !headers.get('x-claude-code-session-id')?.trim()) {
|
|
420
|
+
let bodySession;
|
|
421
|
+
const userID = payload?.metadata?.user_id;
|
|
422
|
+
if (typeof userID === 'string') {
|
|
423
|
+
try {
|
|
424
|
+
const identity = JSON.parse(userID);
|
|
425
|
+
const field = Object.keys(identity ?? {}).find(name => name.toLowerCase() === 'session_id');
|
|
426
|
+
if (field && typeof identity[field] === 'string') bodySession = identity[field];
|
|
427
|
+
} catch { bodySession = /_session_([a-fA-F0-9-]{36})$/.exec(userID)?.[1]; }
|
|
428
|
+
}
|
|
429
|
+
const session = bodySession || hostSession || sessionHeader || (sessionMode === 'reference-stable' ? uuid(base, scope, '', 'claude-session') : undefined);
|
|
430
|
+
if (session) {
|
|
431
|
+
text(session, 'context.session', 512, false);
|
|
432
|
+
set('x-claude-code-session-id', session, bodySession ? 'claude-identity-session' : hostSession ? 'host-session' : 'stable-session');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const h of t.headers) {
|
|
436
|
+
if (!h.enabled || (h.when === 'native' && !native) || (h.when === 'third-party' && native)) continue;
|
|
437
|
+
if (h.operations && !h.operations.includes(operation)) continue;
|
|
438
|
+
if (h.action === 'preserve') continue;
|
|
439
|
+
if (h.action === 'delete' || h.value === null) { del(h.name, 'template-header'); continue; }
|
|
440
|
+
if (h.action === 'set-if-missing' && headers.has(h.name)) continue;
|
|
441
|
+
if (h.action === 'set-if-empty' && headers.get(h.name)?.trim()) continue;
|
|
442
|
+
const value = render(h.value, variables);
|
|
443
|
+
if (value === undefined) { change(`headers.${h.name}`, 'template-variable-unavailable', 'skip'); continue; }
|
|
444
|
+
set(h.name, value, 'template-header');
|
|
445
|
+
}
|
|
446
|
+
if (protocol === 'responses' && !native && ['reference-stable', 'host'].includes(sessionMode)) {
|
|
447
|
+
const given = (headers.get('session-id') ?? headers.get('session_id') ?? '').trim();
|
|
448
|
+
const conversation = hostSession ?? (headers.get('x-session-id') ?? headers.get('x-opencode-session') ?? '').trim();
|
|
449
|
+
const conversationScope = conversation || bodyModel || context.model || '';
|
|
450
|
+
const session = given || hostSession || (sessionMode === 'reference-stable' ? uuid(base, scope, conversationScope, 'session') : undefined);
|
|
451
|
+
if (session) {
|
|
452
|
+
if (!headers.has('session-id')) set('session-id', session, hostSession ? 'host-session' : 'stable-session');
|
|
453
|
+
if (!headers.has('thread-id')) set('thread-id', session, 'stable-session');
|
|
454
|
+
}
|
|
455
|
+
if (sessionMode === 'reference-stable') {
|
|
456
|
+
if (!headers.has('x-codex-window-id')) set('x-codex-window-id', uuid(base, scope, conversationScope, 'window'), 'stable-identity');
|
|
457
|
+
if (!headers.has('x-codex-installation-id')) set('x-codex-installation-id', uuid(base, scope, 'installation'), 'stable-identity');
|
|
458
|
+
if (!hostSession && !sessionHeader) change('context.session', 'codex-session-fallback-unknown', 'derive');
|
|
459
|
+
if (!scope) change('context.credentialScope', 'credential-scope-fallback-unknown', 'derive');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (messages && t.body.fastBeta && payload?.speed === 'fast') {
|
|
463
|
+
const beta = headers.get('anthropic-beta');
|
|
464
|
+
if (!beta?.split(',').some(v => v.trim() === FAST_BETA)) set('anthropic-beta', beta ? `${beta},${FAST_BETA}` : FAST_BETA, 'fast-beta');
|
|
465
|
+
}
|
|
466
|
+
if (rewritten) {
|
|
467
|
+
if (protectedBilling !== undefined && billingSnapshot(payload) !== protectedBilling) throw error('body.parameters', 'Existing billing fields and blocks must remain unchanged');
|
|
468
|
+
resultBody = JSON.stringify(payload);
|
|
469
|
+
if (Buffer.byteLength(resultBody) > t.limits.maxBodyBytes) throw error('body', 'Transformed body exceeds the byte limit');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Host-owned invariants run last, after all optional template rules.
|
|
473
|
+
const nominated = (original.get('connection') ?? '').split(',').map(v => v.trim().toLowerCase()).filter(v => TOKEN.test(v));
|
|
474
|
+
for (const name of [...headers.keys()]) if (protectedHeader(name) || nominated.includes(name)) del(name, 'transport-cleanup');
|
|
475
|
+
if (rewritten) for (const name of ['content-encoding', 'digest', 'content-digest', 'repr-digest', 'content-md5']) del(name, 'body-rewritten');
|
|
476
|
+
signalAbort(signal);
|
|
477
|
+
return { input, init: { ...init, ...(signal ? { signal } : {}), ...(rewritten ? { body: resultBody } : {}), headers }, summary };
|
|
478
|
+
}
|
package/ui/accounts.css
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* 基础层:本页自带主题变量、基础元素与共用控件,不再依赖已退场的 ui/styles.css。 */
|
|
2
|
+
:root{color-scheme:light;--ink:#171717;--muted:#525252;--canvas:#ffffff;--paper:#ffffff;--panel:#f7f7f7;--line:#e5e5e5;--accent:#b45a3c;--accent-soft:#faf0eb;--blue:#b45a3c;--soft:#faf0eb;--positive:#28654d;--negative:#b13c45;--success:#28654d;--error:#b13c45;--danger:#b13c45;--error-bg:#fff5f5;--error-border:#e8c9cc;--warning-bg:#fffaf0;--warning-border:#e7d8b8;--warning-ink:#795516;--input:#ffffff;--input-border:#d4d4d4;--placeholder:#737373;--focus:#b45a3c;--focus-ring:#b45a3c33;--button:#b45a3c;--button-hover:#96472e;--button-ink:#ffffff;--brand-bg:#3b2b24;--brand-ink:#ffffff;--table-head:#f5f5f5;--shadow:#0000001f;--backdrop:#00000066}
|
|
3
|
+
:root[data-theme=dark],:root:not([data-theme=light]){color-scheme:dark;--ink:#ededed;--muted:#a3a3a3;--canvas:#000000;--paper:#000000;--panel:#0d0d0d;--line:#262626;--accent:#e6a184;--accent-soft:#241813;--blue:#e6a184;--soft:#241813;--positive:#8fd4b5;--negative:#f0a4ad;--success:#8fd4b5;--error:#f0a4ad;--danger:#f0a4ad;--error-bg:#1a0f11;--error-border:#5a353b;--warning-bg:#1a1409;--warning-border:#5e4d2e;--warning-ink:#e3c286;--input:#0d0d0d;--input-border:#333333;--placeholder:#8f8f8f;--focus:#e6a184;--focus-ring:#e6a18440;--button:#9a5236;--button-hover:#ac6244;--button-ink:#ffffff;--brand-bg:#3b2b24;--brand-ink:#ffffff;--table-head:#0d0d0d;--shadow:#000000cc;--backdrop:#000000cc}
|
|
4
|
+
*,*::before,*::after{box-sizing:border-box}
|
|
5
|
+
[hidden]{display:none!important}
|
|
6
|
+
body{margin:0;min-height:100vh;overflow-x:clip;background:var(--canvas);color:var(--ink);font-family:\"Segoe UI\",\"Microsoft YaHei\",sans-serif;font-synthesis:none;font-size:14px;line-height:1.6}
|
|
7
|
+
h1,h2,h3,p{margin-top:0}
|
|
8
|
+
h1{margin-bottom:8px;font-size:25px;line-height:1.25;letter-spacing:-.3px}
|
|
9
|
+
h2{margin-bottom:10px;font-size:17px;line-height:1.35}
|
|
10
|
+
p{margin-bottom:10px;line-height:1.65}
|
|
11
|
+
a{color:var(--accent);text-underline-offset:2px}
|
|
12
|
+
a:hover{color:var(--button-hover)}
|
|
13
|
+
nav{display:flex;flex-direction:column;gap:4px;margin:0 0 16px}
|
|
14
|
+
nav a{display:inline-flex;align-items:center;padding:6px 10px;border-radius:6px;color:var(--muted);font-size:13px;text-decoration:none}
|
|
15
|
+
nav a:hover{background:var(--panel);color:var(--ink)}
|
|
16
|
+
nav a[aria-current=page]{color:var(--accent);font-weight:600}
|
|
17
|
+
.brand{display:inline-flex;align-items:center;gap:12px;color:var(--ink);font-size:13px;line-height:1.35;text-decoration:none}
|
|
18
|
+
.brand strong{font-weight:650}
|
|
19
|
+
.brand-icon{flex:none;width:44px;height:44px;border-radius:11px;background:var(--brand-bg)}
|
|
20
|
+
.theme-control{display:inline-flex;flex:none;align-items:center;gap:8px;color:var(--muted);font-size:12px}.theme-control label{white-space:nowrap}
|
|
21
|
+
:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
|
|
22
|
+
button{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:36px;padding:8px 13px;border:1px solid var(--line);border-radius:7px;background:var(--input);color:var(--ink);font:inherit;font-size:13px;font-weight:550;cursor:pointer;text-decoration:none;transition:background-color .15s ease,border-color .15s ease,color .15s ease}
|
|
23
|
+
button:hover:not(:disabled){border-color:var(--input-border);background:var(--accent-soft);color:var(--accent)}
|
|
24
|
+
button:disabled{opacity:.48;cursor:not-allowed}
|
|
25
|
+
.primary{border-color:var(--button);background:var(--button);color:var(--button-ink);font-weight:650}
|
|
26
|
+
.primary:hover:not(:disabled){border-color:var(--button-hover);background:var(--button-hover);color:var(--button-ink)}
|
|
27
|
+
.secondary{border-color:var(--line);background:transparent;color:var(--ink)}
|
|
28
|
+
.secondary:hover:not(:disabled){border-color:var(--input-border);background:var(--accent-soft);color:var(--accent)}
|
|
29
|
+
input,select,textarea{font:inherit;color:var(--ink)}
|
|
30
|
+
input:not([type=checkbox]):not([type=radio]),select,textarea{width:100%;min-width:0;min-height:38px;padding:9px 11px;border:1px solid var(--input-border);border-radius:6px;background:var(--input);font-size:13px;line-height:1.4}
|
|
31
|
+
input::placeholder,textarea::placeholder{color:var(--placeholder)}
|
|
32
|
+
input:not([type=checkbox]):not([type=radio]):focus,select:focus,textarea:focus{border-color:var(--accent);outline:none;box-shadow:0 0 0 3px var(--focus-ring)}
|
|
33
|
+
input[type=checkbox]{flex:none;width:16px;height:16px;margin:0;accent-color:var(--accent);cursor:pointer}
|
|
34
|
+
fieldset{min-width:0;margin:0;padding:0;border:0}
|
|
35
|
+
.field{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
|
|
36
|
+
.field label{color:var(--muted);font-size:12px;font-weight:600}
|
|
37
|
+
.field-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:0 16px;margin-top:18px}
|
|
38
|
+
.hint{margin:4px 0;color:var(--muted);font-size:12px;line-height:1.5}
|
|
39
|
+
.check-label{display:inline-flex;align-items:center;gap:7px;color:var(--muted);font-size:12px;cursor:pointer}
|
|
40
|
+
.secret-meta{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px 16px}
|
|
41
|
+
.completion{max-width:520px;margin:0 auto;padding:72px 24px;text-align:center}
|
|
42
|
+
.completion-icon{display:flex;align-items:center;justify-content:center;width:56px;height:56px;margin:0 auto 18px;border-radius:50%;background:var(--accent-soft);color:var(--accent);font-size:24px;font-weight:700}
|
|
43
|
+
.completion h1{font-size:21px}
|
|
44
|
+
.completion p{margin:0;color:var(--muted);font-size:13px}
|
|
45
|
+
.accounts-shell{max-width:1480px;margin:auto;min-height:100vh}.accounts-header{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:24px 40px;background:var(--paper);border-bottom:1px solid var(--line)}.accounts-header nav{flex-direction:row;gap:10px;margin:0}.accounts-header a[aria-current=page]{background:var(--soft);color:var(--blue)}.accounts-main{padding:42px 40px}.accounts-title{display:flex;justify-content:space-between;align-items:center;gap:24px;margin-bottom:28px}.accounts-title p{font-size:14px;color:var(--muted);margin:0}.account-totals{display:flex;flex-wrap:wrap;gap:16px;margin-bottom:26px}.unit-total{flex:1;min-width:250px;border-left:3px solid var(--blue);padding:12px 20px;background:var(--paper)}.unit-total h2{font-size:14px;margin-bottom:12px}.unit-total .total-values{display:flex;gap:32px;flex-wrap:wrap}.total-metric small{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}.total-metric strong{font-size:24px;font-weight:650;font-variant-numeric:tabular-nums}.unit-total p{font-size:11px;color:var(--muted);margin:12px 0 0}.account-toolbar{display:flex;justify-content:space-between;align-items:center;gap:20px;margin:22px 0 14px}.account-toolbar p{font-size:12px;color:var(--muted);margin:0}.account-toolbar>div{display:flex;gap:10px}.account-table-scroll,.history-scroll{overflow:auto;border:1px solid var(--line);border-radius:8px;background:var(--paper)}table{border-collapse:collapse;width:100%;text-align:left}th{background:var(--table-head);font-size:12px;font-weight:600;color:var(--muted);padding:14px 16px;white-space:nowrap}td{padding:20px 16px;border-top:1px solid var(--line);font-size:13px;vertical-align:top;line-height:1.6}td .primary-value{font-weight:650;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:15px}td .detail{font-size:11px;color:var(--muted);max-width:260px;overflow-wrap:anywhere;margin-top:5px}td .positive{color:var(--positive)}td .negative{color:var(--negative)}.row-actions{display:flex;flex-wrap:wrap;gap:5px;min-width:162px;max-width:240px}.row-actions button{font-size:11px;padding:5px 9px;min-height:30px}.row-actions .danger{color:var(--error)}.account-state{display:inline-block;border-radius:4px;padding:2px 6px;background:var(--soft);font-size:10px;color:var(--blue);margin-top:5px}.account-state.stale{color:var(--warning-ink);background:var(--warning-bg)}.account-notes{max-width:950px;margin-top:20px}.account-notes p{font-size:12px;color:var(--muted);margin-bottom:6px}.account-empty{padding:48px 24px;text-align:center;background:var(--paper);border:1px dashed var(--input-border);border-radius:8px;margin:24px 0}.account-empty h2{font-size:20px}.account-empty p{font-size:13px;color:var(--muted);max-width:540px;margin:0 auto}.accounts-error{margin-bottom:18px;padding:12px 15px;border:1px solid var(--error-border);background:var(--error-bg);color:var(--error);border-radius:6px;font-size:13px;line-height:1.7}dialog{width:min(640px,calc(100% - 32px));max-height:90vh;overflow:auto;border:1px solid var(--line);border-radius:12px;padding:26px;color:var(--ink);background:var(--paper);box-shadow:0 24px 80px var(--shadow)}dialog::backdrop{background:var(--backdrop)}.dialog-header{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:22px}.dialog-header h2{margin:0 0 8px}.dialog-header p{font-size:12px;color:var(--muted);margin:0}.dialog-header button{min-height:32px;padding:4px 12px}.dialog-footer{display:flex;align-items:center;justify-content:space-between;gap:20px;border-top:1px solid var(--line);padding-top:18px;margin-top:24px}.dialog-footer p{margin:0;max-width:320px}.account-toggles{display:flex;flex-direction:column;gap:12px}.dialog-footer button{white-space:nowrap}#account-history{width:min(950px,calc(100% - 32px))}.history-scroll{max-height:55vh}.history-scroll td,.history-scroll th{font-size:12px;white-space:nowrap;padding:12px}.accounts-error:focus{outline:2px solid var(--error);outline-offset:3px}
|
|
46
|
+
.account-schedule{border:1px solid var(--line);border-radius:8px;background:var(--canvas);padding:16px;margin-top:20px}.account-schedule .field-grid{margin-top:18px}.account-schedule .field{margin-bottom:8px}
|
|
47
|
+
@media(max-width:900px){.accounts-header{padding:20px}.accounts-main{padding:30px 20px}.accounts-title{align-items:flex-start}.accounts-header .brand{display:none}.account-totals{gap:12px}.account-toolbar{align-items:flex-start;flex-direction:column}.account-toolbar>div{flex-wrap:wrap}}
|
|
48
|
+
@media(max-width:560px){.accounts-header{gap:10px;padding:15px;flex-wrap:wrap}.accounts-header nav{flex-basis:100%}.accounts-header .theme-control{margin-right:auto}.accounts-header nav a{padding:8px;font-size:12px}.accounts-header>button{font-size:11px;padding:8px}.accounts-main{padding:26px 15px}.accounts-title{flex-direction:column;gap:16px}.accounts-title h1{font-size:24px}.accounts-title p{font-size:12px}.account-totals{flex-direction:column}.unit-total{min-width:0}.account-table-scroll table{min-width:1000px}dialog{padding:20px}.dialog-footer{flex-direction:column;align-items:stretch}.dialog-footer button{width:100%}.accounts-header nav{overflow:visible}}
|
|
49
|
+
.accounts-shell button,#account-editor button,#account-history button,.accounts-header nav a{min-height:44px;display:inline-flex;align-items:center;justify-content:center}
|
|
50
|
+
.accounts-header select{min-height:44px}
|
|
51
|
+
.account-toolbar-status{display:flex;align-items:center;gap:12px;flex-wrap:wrap;min-height:44px}
|
|
52
|
+
.account-toolbar-status p{margin:0}
|
|
53
|
+
.bulk-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
54
|
+
.bulk-reason{font-size:12px;line-height:1.4;color:var(--muted);margin:0;max-width:280px}
|
|
55
|
+
#editor-dirty-state{color:var(--muted)}
|
|
56
|
+
#editor-dirty-state.dirty{color:var(--error);font-weight:600}
|
|
57
|
+
.dialog-footer-note{flex:1;min-width:0}
|
|
58
|
+
.dialog-footer-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}
|
|
59
|
+
.dialog-footer-note .hint{margin:4px 0}
|
|
60
|
+
.accounts-shell [hidden],#account-editor [hidden],#account-history [hidden],#accounts-completion[hidden]{display:none!important}
|
|
61
|
+
@media(max-width:900px){.account-toolbar-status{width:100%}.bulk-actions{width:100%}}
|
|
62
|
+
#account-editor{padding:0;overflow:hidden}
|
|
63
|
+
#account-form{display:flex;flex-direction:column;max-height:90vh;min-height:0}
|
|
64
|
+
#account-editor .dialog-header{flex-shrink:0;padding:20px 22px 12px;margin:0;border-bottom:1px solid var(--line)}
|
|
65
|
+
#account-scroll{flex:1;min-height:0;overflow:auto;padding:18px 22px;scroll-padding:16px;overscroll-behavior:contain}
|
|
66
|
+
#account-fields{min-width:0}
|
|
67
|
+
#account-editor .dialog-footer{flex-shrink:0;margin:0;padding:12px 22px calc(12px + env(safe-area-inset-bottom));background:var(--paper);border-top:1px solid var(--input-border);gap:12px}
|
|
68
|
+
#account-form-error{flex-shrink:0;max-height:18vh;overflow:auto;margin:0;padding:10px 22px;border-radius:0;border-width:0 0 1px}
|
|
69
|
+
#account-editor .dialog-header h2{font-size:20px}
|
|
70
|
+
.reconciliation-summary{flex-shrink:0;max-height:12vh;overflow:auto;font-size:12px;line-height:1.7;padding:10px 20px;background:var(--warning-bg);color:var(--warning-ink);border-bottom:1px solid var(--warning-border);overflow-wrap:anywhere}
|
|
71
|
+
#account-editor .account-field-error{color:var(--error);font-size:12px;line-height:1.6;margin-top:6px}
|
|
72
|
+
@media(max-width:560px){.dialog-footer-actions{flex-direction:column;align-items:stretch}#account-editor .dialog-header{padding:14px 16px 10px}#account-scroll{padding:14px 16px}#account-editor .dialog-footer{padding:10px 16px;gap:8px}.dialog-footer-note .hint{font-size:11px}#account-editor .field input:not([type=checkbox]),#account-editor .field select{font-size:16px}}
|
|
73
|
+
@supports(max-height:90dvh){#account-form{max-height:90dvh}}
|
|
74
|
+
@media(max-height:500px){#account-editor .dialog-header p,#account-editor .dialog-footer-note>.hint:first-child{display:none}#account-editor .dialog-header{padding-top:10px;padding-bottom:6px}#account-editor .dialog-footer{padding-top:8px;padding-bottom:8px;flex-direction:row}}
|
|
75
|
+
@media(max-width:560px){.account-table-scroll{overflow:visible;border:0;border-radius:0;background:transparent}.account-table-scroll table,.account-table-scroll tbody,.account-table-scroll tr,.account-table-scroll td{display:block;width:auto}.account-table-scroll thead{display:none}.account-table-scroll tbody tr{margin-bottom:12px;padding:14px 16px;border:1px solid var(--line);border-radius:8px;background:var(--paper)}.account-table-scroll td{padding:0 0 10px;border:0}.account-table-scroll td:last-child{padding-bottom:0}.account-table-scroll td:not(:first-child)::before{content:attr(data-label);display:block;margin-bottom:2px;color:var(--muted);font-size:11px}.account-table-scroll .row-actions{max-width:none}}
|