-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.ts
More file actions
179 lines (168 loc) · 7.34 KB
/
Copy pathconfig.ts
File metadata and controls
179 lines (168 loc) · 7.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import { z } from 'zod';
import { defaultDataDir } from './lib/paths.js';
export type CompileEngine = 'auto' | 'latexmk' | 'tectonic' | 'pdflatex' | 'xelatex' | 'lualatex';
export interface VibetexConfig {
/** Overleaf git-bridge personal access token (tier 1). */
gitToken?: string;
/** Default Overleaf project id (24-hex). */
projectId?: string;
/** Raw `alias=projectId` comma list (resolved by ProjectRegistry). */
projects?: string;
/** Overleaf web base url (for "Open in Overleaf" / dashboard links). */
baseUrl: string;
/** Overleaf git-bridge base url (default https://git.overleaf.com; Server Pro is `<site>/git`). */
gitBaseUrl: string;
/** Experimental session cookie (`overleaf_session2`); only used when experimentalSession=true. */
sessionCookie?: string;
/** Enable the unofficial, best-effort session-cookie tier (off by default). */
experimentalSession: boolean;
/** Preferred local compile engine; `auto` picks the best available binary. */
compileEngine: CompileEngine;
/** Optional self-hosted CLSI base url for remote compile (tier 3 remote). */
clsiUrl?: string;
/** Hard cap (seconds) on a single local LaTeX compile run. */
latexTimeoutSec: number;
dataDir: string;
contactEmail?: string;
allowDelete: boolean;
readOnly: boolean;
logLevel: 'debug' | 'info' | 'warn' | 'error';
logFormat: 'text' | 'json';
allowInsecureHttp: boolean;
metricsEnabled: boolean;
/** When true, /readyz also pings the Overleaf base host. */
readyzCheckOverleaf: boolean;
mcpRateLimit: { windowMs: number; max: number };
oauth: {
enabled: boolean;
publicUrl?: string;
passcode?: string;
accessTokenTtlSec: number;
refreshTokenTtlSec: number;
allowedHosts: string[];
mode: 'passcode';
store: 'memory' | 'file';
tokenSecret?: string;
};
}
/** Minimum length for the consent passcode (defense-in-depth alongside /consent throttling). */
export const MIN_PASSCODE_LENGTH = 12;
const bool = (def: boolean) =>
z
.string()
.optional()
.transform((v) => (v === undefined ? def : v.toLowerCase() === 'true' || v === '1'));
/**
* A string env var that is optional but, when present, must be non-empty.
* Treats an empty string (a bare `KEY=` line in a .env file) as unset rather
* than a `min(1)` violation, so empty entries don't crash boot. Where the value
* is actually required (e.g. OAuth passcode/secret), the cross-field checks
* below surface a clear error instead of a cryptic parse failure.
*/
const optionalNonEmpty = () =>
z.preprocess((v) => (v === '' ? undefined : v), z.string().min(1).optional());
export function loadConfig(env: NodeJS.ProcessEnv = process.env): VibetexConfig {
const schema = z.object({
OVERLEAF_GIT_TOKEN: optionalNonEmpty(),
OVERLEAF_PROJECT_ID: optionalNonEmpty(),
OVERLEAF_PROJECTS: optionalNonEmpty(),
OVERLEAF_BASE_URL: z.string().url().default('https://www.overleaf.com'),
OVERLEAF_GIT_BASE_URL: z.string().url().default('https://git.overleaf.com'),
OVERLEAF_SESSION_COOKIE: optionalNonEmpty(),
VIBETEX_EXPERIMENTAL_SESSION: bool(false),
VIBETEX_COMPILE_ENGINE: z
.enum(['auto', 'latexmk', 'tectonic', 'pdflatex', 'xelatex', 'lualatex'])
.default('auto'),
VIBETEX_CLSI_URL: optionalNonEmpty(),
VIBETEX_LATEX_TIMEOUT_SEC: z.coerce.number().int().positive().default(120),
VIBETEX_DATA_DIR: z.string().optional(),
VIBETEX_CONTACT_EMAIL: z.string().email().optional(),
VIBETEX_ALLOW_DELETE: bool(false),
VIBETEX_READ_ONLY: bool(false),
VIBETEX_LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
VIBETEX_LOG_FORMAT: z.enum(['text', 'json']).default('text'),
VIBETEX_ALLOW_INSECURE_HTTP: bool(false),
VIBETEX_METRICS_ENABLED: bool(false),
VIBETEX_READYZ_CHECK_OVERLEAF: bool(true),
VIBETEX_MCP_RATE_LIMIT_WINDOW_SEC: z.coerce.number().int().nonnegative().default(60),
VIBETEX_MCP_RATE_LIMIT_MAX: z.coerce.number().int().nonnegative().default(120),
VIBETEX_OAUTH_ENABLED: bool(false),
VIBETEX_PUBLIC_URL: z.string().url().optional(),
VIBETEX_OAUTH_PASSCODE: optionalNonEmpty(),
VIBETEX_OAUTH_ACCESS_TTL: z.coerce.number().int().positive().default(3600),
VIBETEX_OAUTH_REFRESH_TTL: z.coerce.number().int().positive().default(2592000),
VIBETEX_ALLOWED_HOSTS: z.string().optional(),
VIBETEX_OAUTH_MODE: z.enum(['passcode']).default('passcode'),
VIBETEX_OAUTH_STORE: z.enum(['memory', 'file']).default('memory'),
VIBETEX_OAUTH_TOKEN_SECRET: optionalNonEmpty(),
});
const parsed = schema.parse(env);
const oauthEnabled = parsed.VIBETEX_OAUTH_ENABLED;
const publicUrl = parsed.VIBETEX_PUBLIC_URL?.replace(/\/+$/, '');
const mode = parsed.VIBETEX_OAUTH_MODE;
const store = parsed.VIBETEX_OAUTH_STORE;
if (oauthEnabled) {
if (!publicUrl)
throw new Error('VIBETEX_PUBLIC_URL is required when VIBETEX_OAUTH_ENABLED=true');
if (!parsed.VIBETEX_OAUTH_PASSCODE) {
throw new Error(
'VIBETEX_OAUTH_PASSCODE is required when VIBETEX_OAUTH_ENABLED=true (passcode mode)',
);
}
if (parsed.VIBETEX_OAUTH_PASSCODE.length < MIN_PASSCODE_LENGTH) {
throw new Error(
`VIBETEX_OAUTH_PASSCODE must be at least ${MIN_PASSCODE_LENGTH} characters (generate one with: openssl rand -base64 24)`,
);
}
if (store === 'file' && !parsed.VIBETEX_OAUTH_TOKEN_SECRET) {
throw new Error(
'VIBETEX_OAUTH_TOKEN_SECRET is required when VIBETEX_OAUTH_STORE=file (used to encrypt stored tokens at rest; generate one with: openssl rand -base64 32)',
);
}
}
if (parsed.VIBETEX_EXPERIMENTAL_SESSION && !parsed.OVERLEAF_SESSION_COOKIE) {
throw new Error(
'VIBETEX_EXPERIMENTAL_SESSION=true requires OVERLEAF_SESSION_COOKIE (paste your own `overleaf_session2` cookie value). The session tier is unofficial and best-effort.',
);
}
const allowedHosts = (parsed.VIBETEX_ALLOWED_HOSTS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
return {
gitToken: parsed.OVERLEAF_GIT_TOKEN,
projectId: parsed.OVERLEAF_PROJECT_ID,
projects: parsed.OVERLEAF_PROJECTS,
baseUrl: parsed.OVERLEAF_BASE_URL.replace(/\/+$/, ''),
gitBaseUrl: parsed.OVERLEAF_GIT_BASE_URL.replace(/\/+$/, ''),
sessionCookie: parsed.OVERLEAF_SESSION_COOKIE,
experimentalSession: parsed.VIBETEX_EXPERIMENTAL_SESSION,
compileEngine: parsed.VIBETEX_COMPILE_ENGINE,
clsiUrl: parsed.VIBETEX_CLSI_URL?.replace(/\/+$/, ''),
latexTimeoutSec: parsed.VIBETEX_LATEX_TIMEOUT_SEC,
dataDir: parsed.VIBETEX_DATA_DIR ?? defaultDataDir(env),
contactEmail: parsed.VIBETEX_CONTACT_EMAIL,
allowDelete: parsed.VIBETEX_ALLOW_DELETE,
readOnly: parsed.VIBETEX_READ_ONLY,
logLevel: parsed.VIBETEX_LOG_LEVEL,
logFormat: parsed.VIBETEX_LOG_FORMAT,
allowInsecureHttp: parsed.VIBETEX_ALLOW_INSECURE_HTTP,
metricsEnabled: parsed.VIBETEX_METRICS_ENABLED,
readyzCheckOverleaf: parsed.VIBETEX_READYZ_CHECK_OVERLEAF,
mcpRateLimit: {
windowMs: parsed.VIBETEX_MCP_RATE_LIMIT_WINDOW_SEC * 1000,
max: parsed.VIBETEX_MCP_RATE_LIMIT_MAX,
},
oauth: {
enabled: oauthEnabled,
publicUrl,
passcode: parsed.VIBETEX_OAUTH_PASSCODE,
accessTokenTtlSec: parsed.VIBETEX_OAUTH_ACCESS_TTL,
refreshTokenTtlSec: parsed.VIBETEX_OAUTH_REFRESH_TTL,
allowedHosts,
mode,
store,
tokenSecret: parsed.VIBETEX_OAUTH_TOKEN_SECRET,
},
};
}