@levr-one/auth 1.0.0
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/README.md +37 -0
- package/dist/config-watcher.d.ts +65 -0
- package/dist/config-watcher.d.ts.map +1 -0
- package/dist/config-watcher.js +140 -0
- package/dist/config-watcher.js.map +1 -0
- package/dist/config-watcher.test.d.ts +2 -0
- package/dist/config-watcher.test.d.ts.map +1 -0
- package/dist/config-watcher.test.js +116 -0
- package/dist/config-watcher.test.js.map +1 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +234 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +221 -0
- package/dist/config.test.js.map +1 -0
- package/dist/file-lock.d.ts +41 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +110 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth-client.d.ts +156 -0
- package/dist/oauth-client.d.ts.map +1 -0
- package/dist/oauth-client.js +572 -0
- package/dist/oauth-client.js.map +1 -0
- package/dist/oauth-client.test.d.ts +2 -0
- package/dist/oauth-client.test.d.ts.map +1 -0
- package/dist/oauth-client.test.js +290 -0
- package/dist/oauth-client.test.js.map +1 -0
- package/dist/test-utils.d.ts +10 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +45 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/token-store.d.ts +78 -0
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +299 -0
- package/dist/token-store.js.map +1 -0
- package/dist/token-store.test.d.ts +2 -0
- package/dist/token-store.test.d.ts.map +1 -0
- package/dist/token-store.test.js +239 -0
- package/dist/token-store.test.js.map +1 -0
- package/dist/token-watcher.d.ts +82 -0
- package/dist/token-watcher.d.ts.map +1 -0
- package/dist/token-watcher.js +185 -0
- package/dist/token-watcher.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-store.d.ts +60 -0
- package/dist/workspace-store.d.ts.map +1 -0
- package/dist/workspace-store.js +147 -0
- package/dist/workspace-store.js.map +1 -0
- package/dist/workspace-store.test.d.ts +2 -0
- package/dist/workspace-store.test.d.ts.map +1 -0
- package/dist/workspace-store.test.js +169 -0
- package/dist/workspace-store.test.js.map +1 -0
- package/dist/workspaces.d.ts +60 -0
- package/dist/workspaces.d.ts.map +1 -0
- package/dist/workspaces.js +80 -0
- package/dist/workspaces.js.map +1 -0
- package/dist/workspaces.test.d.ts +2 -0
- package/dist/workspaces.test.d.ts.map +1 -0
- package/dist/workspaces.test.js +111 -0
- package/dist/workspaces.test.js.map +1 -0
- package/package.json +58 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified environment configuration for TQ.AI
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for backend URLs, auth URLs, and OAuth client IDs.
|
|
5
|
+
* Precedence: TQ_BACKEND_URL env var > ~/.tq/config.json > token apiBaseUrl > staging default
|
|
6
|
+
*
|
|
7
|
+
* In test environments (VITEST or NODE_ENV=test), ~/.tq/config.json is
|
|
8
|
+
* skipped so that tests stay deterministic and never depend on the
|
|
9
|
+
* developer's local config file.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { getTqDir, loadTokens } from './token-store.js';
|
|
14
|
+
/**
|
|
15
|
+
* Returns true when running inside a test runner (Vitest, Jest, etc.).
|
|
16
|
+
*/
|
|
17
|
+
function isTestEnv() {
|
|
18
|
+
return (process.env.VITEST !== undefined ||
|
|
19
|
+
process.env.NODE_ENV === 'test' ||
|
|
20
|
+
process.env.JEST_WORKER_ID !== undefined);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Detect whether the developer is running in TLS dev mode (mkcert + DEV_TLS).
|
|
24
|
+
*
|
|
25
|
+
* Resolution order:
|
|
26
|
+
* 1. Tests (Vitest/Jest/NODE_ENV=test) → always false (deterministic).
|
|
27
|
+
* 2. Explicit `DEV_TLS` env var → 'true'/'false' wins.
|
|
28
|
+
* 3. Cert files at apps/backender/certs/ → presence ⇒ TLS mode.
|
|
29
|
+
*
|
|
30
|
+
* The cert-file probe walks up from cwd until it finds the repo root or
|
|
31
|
+
* hits the filesystem root, so this works whether the CLI is run from
|
|
32
|
+
* the repo root, an app subdirectory, or a worktree.
|
|
33
|
+
*/
|
|
34
|
+
function isLocalTlsMode() {
|
|
35
|
+
if (isTestEnv())
|
|
36
|
+
return false;
|
|
37
|
+
if (process.env.DEV_TLS === 'true')
|
|
38
|
+
return true;
|
|
39
|
+
if (process.env.DEV_TLS === 'false')
|
|
40
|
+
return false;
|
|
41
|
+
let dir = process.cwd();
|
|
42
|
+
for (let i = 0; i < 10; i++) {
|
|
43
|
+
if (fs.existsSync(path.join(dir, 'apps/backender/certs/dev.crt'))) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const parent = path.dirname(dir);
|
|
47
|
+
if (parent === dir)
|
|
48
|
+
break;
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const LOCAL_HTTP = {
|
|
54
|
+
environment: 'local',
|
|
55
|
+
apiUrl: 'http://localhost:8080',
|
|
56
|
+
authUrl: 'http://localhost:3021',
|
|
57
|
+
clientUrl: 'http://localhost:3020',
|
|
58
|
+
oauthClientId: '2',
|
|
59
|
+
};
|
|
60
|
+
const LOCAL_HTTPS = {
|
|
61
|
+
environment: 'local',
|
|
62
|
+
// internal D14: TLS dev mode mirrors the prod parent-domain topology
|
|
63
|
+
// (`*.levr.test` is the dev counterpart of `*.levr.now` / `*.levr.one`).
|
|
64
|
+
// The mkcert cert generated by scripts/dev-setup-tls.sh covers these
|
|
65
|
+
// hostnames, /etc/hosts entries are written by the same script, and the
|
|
66
|
+
// SPA's `runtime-config.ts` falls back to the same triple. Keeping the
|
|
67
|
+
// names consistent end-to-end means cookies set with `Domain=.levr.test`
|
|
68
|
+
// travel to all three subdomains the same way they do in prod.
|
|
69
|
+
apiUrl: 'https://api.levr.test:8080',
|
|
70
|
+
authUrl: 'https://auth.levr.test:3021',
|
|
71
|
+
clientUrl: 'https://ai.levr.test:3020',
|
|
72
|
+
oauthClientId: '2',
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Environment presets. Staging is the default.
|
|
76
|
+
*
|
|
77
|
+
* `local` is a getter that returns the HTTPS variant when DEV_TLS dev mode
|
|
78
|
+
* is detected (see `isLocalTlsMode()`), so `yarn tq:env local` writes the
|
|
79
|
+
* right URLs whether the user has TLS dev mode set up or not.
|
|
80
|
+
*/
|
|
81
|
+
export const PRESETS = {
|
|
82
|
+
staging: {
|
|
83
|
+
environment: 'staging',
|
|
84
|
+
// internal D15: levr.now is the unified staging origin set.
|
|
85
|
+
apiUrl: 'https://api.levr.now',
|
|
86
|
+
authUrl: 'https://auth.levr.now',
|
|
87
|
+
clientUrl: 'https://ai.levr.now',
|
|
88
|
+
oauthClientId: '2',
|
|
89
|
+
},
|
|
90
|
+
get local() {
|
|
91
|
+
return isLocalTlsMode() ? { ...LOCAL_HTTPS } : { ...LOCAL_HTTP };
|
|
92
|
+
},
|
|
93
|
+
production: {
|
|
94
|
+
environment: 'production',
|
|
95
|
+
// internal: rebranded levr.tools → levr.one. Redirect CloudFront keeps
|
|
96
|
+
// legacy levr.tools URLs reachable indefinitely via 301.
|
|
97
|
+
apiUrl: 'https://api.levr.one',
|
|
98
|
+
authUrl: 'https://auth.levr.one',
|
|
99
|
+
clientUrl: 'https://ai.levr.one',
|
|
100
|
+
oauthClientId: '2',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Env var override — single canonical name
|
|
105
|
+
*/
|
|
106
|
+
const ENV_VAR = 'TQ_BACKEND_URL';
|
|
107
|
+
/**
|
|
108
|
+
* Legacy localhost form for the HTTPS dev preset. Older configs and
|
|
109
|
+
* stored tokens may have `https://localhost:8080` as the apiUrl; we
|
|
110
|
+
* accept it as an alias of `LOCAL_HTTPS` so flipping DEV_TLS doesn't
|
|
111
|
+
* orphan existing config files.
|
|
112
|
+
*/
|
|
113
|
+
const LOCAL_HTTPS_LOCALHOST_ALIAS = 'https://localhost:8080';
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a full TqConfig from a bare API URL by matching presets.
|
|
116
|
+
* Returns 'custom' environment if no preset matches.
|
|
117
|
+
*
|
|
118
|
+
* Local resolution covers three forms:
|
|
119
|
+
* - `http://localhost:8080` → LOCAL_HTTP
|
|
120
|
+
* - `https://api.levr.test:8080` → LOCAL_HTTPS (canonical TLS form)
|
|
121
|
+
* - `https://localhost:8080` → LOCAL_HTTPS (legacy alias)
|
|
122
|
+
*
|
|
123
|
+
* Resolving the legacy localhost alias to the new levr.test preset means
|
|
124
|
+
* a token issued in TLS mode under the old localhost URL still maps to
|
|
125
|
+
* the right config when read after this change.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveFromApiUrl(url) {
|
|
128
|
+
const normalized = url.replace(/\/+$/, '');
|
|
129
|
+
if (normalized === LOCAL_HTTP.apiUrl)
|
|
130
|
+
return { ...LOCAL_HTTP };
|
|
131
|
+
if (normalized === LOCAL_HTTPS.apiUrl)
|
|
132
|
+
return { ...LOCAL_HTTPS };
|
|
133
|
+
if (normalized === LOCAL_HTTPS_LOCALHOST_ALIAS)
|
|
134
|
+
return { ...LOCAL_HTTPS };
|
|
135
|
+
for (const preset of [PRESETS.staging, PRESETS.production]) {
|
|
136
|
+
if (preset.apiUrl === normalized) {
|
|
137
|
+
return { ...preset };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Custom URL -- use staging-style auth/client URLs as placeholders
|
|
141
|
+
return {
|
|
142
|
+
environment: 'custom',
|
|
143
|
+
apiUrl: normalized,
|
|
144
|
+
authUrl: normalized,
|
|
145
|
+
clientUrl: normalized,
|
|
146
|
+
oauthClientId: process.env.TQ_OAUTH_CLIENT_ID || '2',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Load unified config with precedence:
|
|
151
|
+
* 1. TQ_BACKEND_URL env var
|
|
152
|
+
* 2. ~/.tq/config.json (skipped in test environments)
|
|
153
|
+
* 3. Token's apiBaseUrl from ~/.tq/oauth-tokens.json (skipped in test environments)
|
|
154
|
+
* 4. Staging default
|
|
155
|
+
*
|
|
156
|
+
* TQ_OAUTH_CLIENT_ID env var overrides oauthClientId from any source.
|
|
157
|
+
*/
|
|
158
|
+
export function loadConfig() {
|
|
159
|
+
let config;
|
|
160
|
+
// 1. Check env var override
|
|
161
|
+
const envValue = process.env[ENV_VAR];
|
|
162
|
+
if (envValue) {
|
|
163
|
+
config = resolveFromApiUrl(envValue);
|
|
164
|
+
if (process.env.TQ_OAUTH_CLIENT_ID) {
|
|
165
|
+
config.oauthClientId = process.env.TQ_OAUTH_CLIENT_ID;
|
|
166
|
+
}
|
|
167
|
+
return config;
|
|
168
|
+
}
|
|
169
|
+
// 2. Read ~/.tq/config.json (skip in test environments for determinism)
|
|
170
|
+
if (!isTestEnv()) {
|
|
171
|
+
const configPath = getConfigFilePath();
|
|
172
|
+
if (fs.existsSync(configPath)) {
|
|
173
|
+
try {
|
|
174
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
175
|
+
config = {
|
|
176
|
+
environment: raw.environment || 'custom',
|
|
177
|
+
apiUrl: raw.apiUrl ?? PRESETS.staging.apiUrl,
|
|
178
|
+
authUrl: raw.authUrl ?? PRESETS.staging.authUrl,
|
|
179
|
+
clientUrl: raw.clientUrl ?? PRESETS.staging.clientUrl,
|
|
180
|
+
oauthClientId: raw.oauthClientId ?? '2',
|
|
181
|
+
};
|
|
182
|
+
// Apply TQ_OAUTH_CLIENT_ID override
|
|
183
|
+
if (process.env.TQ_OAUTH_CLIENT_ID) {
|
|
184
|
+
config.oauthClientId = process.env.TQ_OAUTH_CLIENT_ID;
|
|
185
|
+
}
|
|
186
|
+
return config;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
process.stderr.write(`[tq-oauth] WARNING: Malformed config at ${configPath}, falling back to default.\n`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// 3. Infer from stored token's apiBaseUrl (tokens know which backend issued them)
|
|
194
|
+
if (!isTestEnv()) {
|
|
195
|
+
const stored = loadTokens();
|
|
196
|
+
if (stored?.apiBaseUrl) {
|
|
197
|
+
config = resolveFromApiUrl(stored.apiBaseUrl);
|
|
198
|
+
if (process.env.TQ_OAUTH_CLIENT_ID) {
|
|
199
|
+
config.oauthClientId = process.env.TQ_OAUTH_CLIENT_ID;
|
|
200
|
+
}
|
|
201
|
+
process.stderr.write(`[tq-oauth] No config.json found. Using environment from token: ${config.environment} (${config.apiUrl})\n`);
|
|
202
|
+
return config;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// 4. Default to staging
|
|
206
|
+
config = { ...PRESETS.staging };
|
|
207
|
+
// Apply TQ_OAUTH_CLIENT_ID override
|
|
208
|
+
if (process.env.TQ_OAUTH_CLIENT_ID) {
|
|
209
|
+
config.oauthClientId = process.env.TQ_OAUTH_CLIENT_ID;
|
|
210
|
+
}
|
|
211
|
+
return config;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Write config to ~/.tq/config.json using atomic write (tmp + rename)
|
|
215
|
+
*/
|
|
216
|
+
export function writeConfig(config) {
|
|
217
|
+
const tqDir = getTqDir();
|
|
218
|
+
if (!fs.existsSync(tqDir)) {
|
|
219
|
+
fs.mkdirSync(tqDir, { recursive: true, mode: 0o700 });
|
|
220
|
+
}
|
|
221
|
+
const configPath = getConfigFilePath();
|
|
222
|
+
const tempPath = configPath + '.tmp';
|
|
223
|
+
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', {
|
|
224
|
+
mode: 0o600,
|
|
225
|
+
});
|
|
226
|
+
fs.renameSync(tempPath, configPath);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get the config file path (~/.tq/config.json)
|
|
230
|
+
*/
|
|
231
|
+
export function getConfigFilePath() {
|
|
232
|
+
return path.join(getTqDir(), 'config.json');
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../src/config.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
// Mutable ref so the mock factory can read the current tmpDir
|
|
6
|
+
const state = vi.hoisted(() => ({ tmpDir: '' }));
|
|
7
|
+
// Mock token-store — config.ts imports getTqDir and loadTokens from it
|
|
8
|
+
vi.mock('./token-store.js', () => ({
|
|
9
|
+
getTqDir: () => path.join(state.tmpDir, '.tq'),
|
|
10
|
+
loadTokens: () => null,
|
|
11
|
+
}));
|
|
12
|
+
import { loadConfig, writeConfig, resolveFromApiUrl, getConfigFilePath, PRESETS, } from './config.js';
|
|
13
|
+
/**
|
|
14
|
+
* Temporarily simulate a non-test environment so loadConfig()
|
|
15
|
+
* exercises the config-file reading path.
|
|
16
|
+
*/
|
|
17
|
+
function withNonTestEnv(fn) {
|
|
18
|
+
const saved = {
|
|
19
|
+
VITEST: process.env.VITEST,
|
|
20
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
21
|
+
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
|
22
|
+
};
|
|
23
|
+
delete process.env.VITEST;
|
|
24
|
+
process.env.NODE_ENV = 'development';
|
|
25
|
+
delete process.env.JEST_WORKER_ID;
|
|
26
|
+
try {
|
|
27
|
+
return fn();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (saved.VITEST !== undefined)
|
|
31
|
+
process.env.VITEST = saved.VITEST;
|
|
32
|
+
else
|
|
33
|
+
delete process.env.VITEST;
|
|
34
|
+
if (saved.NODE_ENV !== undefined)
|
|
35
|
+
process.env.NODE_ENV = saved.NODE_ENV;
|
|
36
|
+
else
|
|
37
|
+
delete process.env.NODE_ENV;
|
|
38
|
+
if (saved.JEST_WORKER_ID !== undefined)
|
|
39
|
+
process.env.JEST_WORKER_ID = saved.JEST_WORKER_ID;
|
|
40
|
+
else
|
|
41
|
+
delete process.env.JEST_WORKER_ID;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
state.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tq-oauth-test-'));
|
|
46
|
+
// Clear relevant env vars
|
|
47
|
+
delete process.env.TQ_BACKEND_URL;
|
|
48
|
+
delete process.env.TQ_OAUTH_CLIENT_ID;
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
fs.rmSync(state.tmpDir, { recursive: true, force: true });
|
|
52
|
+
delete process.env.TQ_BACKEND_URL;
|
|
53
|
+
delete process.env.TQ_OAUTH_CLIENT_ID;
|
|
54
|
+
});
|
|
55
|
+
// ---------- resolveFromApiUrl ----------
|
|
56
|
+
describe('resolveFromApiUrl', () => {
|
|
57
|
+
it('returns staging preset for staging URL', () => {
|
|
58
|
+
const config = resolveFromApiUrl('https://api.levr.now');
|
|
59
|
+
expect(config.environment).toBe('staging');
|
|
60
|
+
expect(config.apiUrl).toBe('https://api.levr.now');
|
|
61
|
+
expect(config.authUrl).toBe('https://auth.levr.now');
|
|
62
|
+
expect(config.clientUrl).toBe('https://ai.levr.now');
|
|
63
|
+
});
|
|
64
|
+
it('returns local preset for localhost URL', () => {
|
|
65
|
+
const config = resolveFromApiUrl('http://localhost:8080');
|
|
66
|
+
expect(config.environment).toBe('local');
|
|
67
|
+
expect(config.authUrl).toBe('http://localhost:3021');
|
|
68
|
+
});
|
|
69
|
+
it('returns local preset for canonical TLS URL (api.levr.test)', () => {
|
|
70
|
+
const config = resolveFromApiUrl('https://api.levr.test:8080');
|
|
71
|
+
expect(config.environment).toBe('local');
|
|
72
|
+
expect(config.apiUrl).toBe('https://api.levr.test:8080');
|
|
73
|
+
expect(config.authUrl).toBe('https://auth.levr.test:3021');
|
|
74
|
+
expect(config.clientUrl).toBe('https://ai.levr.test:3020');
|
|
75
|
+
});
|
|
76
|
+
it('treats legacy https://localhost:8080 as an alias of LOCAL_HTTPS', () => {
|
|
77
|
+
// Tokens issued before LOCAL_HTTPS adopted *.levr.test still need to
|
|
78
|
+
// resolve to 'local' so users don't fall through to 'custom' on
|
|
79
|
+
// upgrade. The alias maps to the NEW canonical URLs.
|
|
80
|
+
const config = resolveFromApiUrl('https://localhost:8080');
|
|
81
|
+
expect(config.environment).toBe('local');
|
|
82
|
+
expect(config.apiUrl).toBe('https://api.levr.test:8080');
|
|
83
|
+
expect(config.authUrl).toBe('https://auth.levr.test:3021');
|
|
84
|
+
expect(config.clientUrl).toBe('https://ai.levr.test:3020');
|
|
85
|
+
});
|
|
86
|
+
it('returns production preset for production URL', () => {
|
|
87
|
+
const config = resolveFromApiUrl('https://api.levr.one');
|
|
88
|
+
expect(config.environment).toBe('production');
|
|
89
|
+
expect(config.authUrl).toBe('https://auth.levr.one');
|
|
90
|
+
});
|
|
91
|
+
it('strips trailing slashes before matching', () => {
|
|
92
|
+
const config = resolveFromApiUrl('http://localhost:8080///');
|
|
93
|
+
expect(config.environment).toBe('local');
|
|
94
|
+
});
|
|
95
|
+
it('returns custom environment for unknown URLs', () => {
|
|
96
|
+
const config = resolveFromApiUrl('https://my-backend.example.com');
|
|
97
|
+
expect(config.environment).toBe('custom');
|
|
98
|
+
expect(config.apiUrl).toBe('https://my-backend.example.com');
|
|
99
|
+
expect(config.oauthClientId).toBe('2');
|
|
100
|
+
});
|
|
101
|
+
it('returns a fresh copy for each call', () => {
|
|
102
|
+
const a = resolveFromApiUrl('http://localhost:8080');
|
|
103
|
+
const b = resolveFromApiUrl('http://localhost:8080');
|
|
104
|
+
expect(a).not.toBe(b);
|
|
105
|
+
expect(a).toEqual(b);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// ---------- loadConfig ----------
|
|
109
|
+
describe('loadConfig', () => {
|
|
110
|
+
it('defaults to staging when no env var and no config file', () => {
|
|
111
|
+
const config = loadConfig();
|
|
112
|
+
expect(config).toEqual(PRESETS.staging);
|
|
113
|
+
});
|
|
114
|
+
it('uses TQ_BACKEND_URL env var when set', () => {
|
|
115
|
+
process.env.TQ_BACKEND_URL = 'http://localhost:8080';
|
|
116
|
+
const config = loadConfig();
|
|
117
|
+
expect(config.environment).toBe('local');
|
|
118
|
+
expect(config.apiUrl).toBe('http://localhost:8080');
|
|
119
|
+
});
|
|
120
|
+
it('env var takes precedence over config file', () => {
|
|
121
|
+
const tqDir = path.join(state.tmpDir, '.tq');
|
|
122
|
+
fs.mkdirSync(tqDir, { recursive: true });
|
|
123
|
+
fs.writeFileSync(path.join(tqDir, 'config.json'), JSON.stringify(PRESETS.staging));
|
|
124
|
+
process.env.TQ_BACKEND_URL = 'http://localhost:8080';
|
|
125
|
+
const config = loadConfig();
|
|
126
|
+
expect(config.environment).toBe('local');
|
|
127
|
+
});
|
|
128
|
+
it('reads config from ~/.tq/config.json when no env var (non-test env)', () => {
|
|
129
|
+
const tqDir = path.join(state.tmpDir, '.tq');
|
|
130
|
+
fs.mkdirSync(tqDir, { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(tqDir, 'config.json'), JSON.stringify(PRESETS.production));
|
|
132
|
+
const config = withNonTestEnv(() => loadConfig());
|
|
133
|
+
expect(config.environment).toBe('production');
|
|
134
|
+
expect(config.apiUrl).toBe('https://api.levr.one');
|
|
135
|
+
});
|
|
136
|
+
it('skips config file in test environments and defaults to staging', () => {
|
|
137
|
+
const tqDir = path.join(state.tmpDir, '.tq');
|
|
138
|
+
fs.mkdirSync(tqDir, { recursive: true });
|
|
139
|
+
fs.writeFileSync(path.join(tqDir, 'config.json'), JSON.stringify(PRESETS.production));
|
|
140
|
+
// VITEST env var is set by vitest — loadConfig should skip the file
|
|
141
|
+
const config = loadConfig();
|
|
142
|
+
expect(config).toEqual(PRESETS.staging);
|
|
143
|
+
});
|
|
144
|
+
it('falls back to staging on malformed config.json (non-test env)', () => {
|
|
145
|
+
const tqDir = path.join(state.tmpDir, '.tq');
|
|
146
|
+
fs.mkdirSync(tqDir, { recursive: true });
|
|
147
|
+
fs.writeFileSync(path.join(tqDir, 'config.json'), 'not json{{{');
|
|
148
|
+
const config = withNonTestEnv(() => loadConfig());
|
|
149
|
+
expect(config).toEqual(PRESETS.staging);
|
|
150
|
+
});
|
|
151
|
+
it('applies TQ_OAUTH_CLIENT_ID override with env var source', () => {
|
|
152
|
+
process.env.TQ_BACKEND_URL = 'http://localhost:8080';
|
|
153
|
+
process.env.TQ_OAUTH_CLIENT_ID = '99';
|
|
154
|
+
const config = loadConfig();
|
|
155
|
+
expect(config.oauthClientId).toBe('99');
|
|
156
|
+
});
|
|
157
|
+
it('applies TQ_OAUTH_CLIENT_ID override with config file source (non-test env)', () => {
|
|
158
|
+
const tqDir = path.join(state.tmpDir, '.tq');
|
|
159
|
+
fs.mkdirSync(tqDir, { recursive: true });
|
|
160
|
+
fs.writeFileSync(path.join(tqDir, 'config.json'), JSON.stringify(PRESETS.local));
|
|
161
|
+
process.env.TQ_OAUTH_CLIENT_ID = '42';
|
|
162
|
+
const config = withNonTestEnv(() => loadConfig());
|
|
163
|
+
expect(config.oauthClientId).toBe('42');
|
|
164
|
+
});
|
|
165
|
+
it('applies TQ_OAUTH_CLIENT_ID override with default source', () => {
|
|
166
|
+
process.env.TQ_OAUTH_CLIENT_ID = '7';
|
|
167
|
+
const config = loadConfig();
|
|
168
|
+
expect(config.oauthClientId).toBe('7');
|
|
169
|
+
expect(config.environment).toBe('staging');
|
|
170
|
+
});
|
|
171
|
+
it('defaults oauthClientId to 2 when missing from config file (non-test env)', () => {
|
|
172
|
+
const tqDir = path.join(state.tmpDir, '.tq');
|
|
173
|
+
fs.mkdirSync(tqDir, { recursive: true });
|
|
174
|
+
fs.writeFileSync(path.join(tqDir, 'config.json'), JSON.stringify({
|
|
175
|
+
environment: 'local',
|
|
176
|
+
apiUrl: 'http://localhost:8080',
|
|
177
|
+
authUrl: 'http://localhost:3021',
|
|
178
|
+
clientUrl: 'http://localhost:3020',
|
|
179
|
+
}));
|
|
180
|
+
const config = withNonTestEnv(() => loadConfig());
|
|
181
|
+
expect(config.oauthClientId).toBe('2');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
// ---------- writeConfig ----------
|
|
185
|
+
describe('writeConfig', () => {
|
|
186
|
+
it('creates ~/.tq directory and writes config.json', () => {
|
|
187
|
+
writeConfig(PRESETS.local);
|
|
188
|
+
const configPath = getConfigFilePath();
|
|
189
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
190
|
+
const written = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
191
|
+
expect(written.environment).toBe('local');
|
|
192
|
+
expect(written.apiUrl).toBe('http://localhost:8080');
|
|
193
|
+
});
|
|
194
|
+
it('overwrites existing config', () => {
|
|
195
|
+
writeConfig(PRESETS.local);
|
|
196
|
+
writeConfig(PRESETS.staging);
|
|
197
|
+
const configPath = getConfigFilePath();
|
|
198
|
+
const written = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
199
|
+
expect(written.environment).toBe('staging');
|
|
200
|
+
});
|
|
201
|
+
it('does not leave temp files after write', () => {
|
|
202
|
+
writeConfig(PRESETS.local);
|
|
203
|
+
const tqDir = path.join(state.tmpDir, '.tq');
|
|
204
|
+
const files = fs.readdirSync(tqDir);
|
|
205
|
+
expect(files).not.toContain('config.json.tmp');
|
|
206
|
+
expect(files).toContain('config.json');
|
|
207
|
+
});
|
|
208
|
+
it('round-trips through writeConfig and loadConfig (non-test env)', () => {
|
|
209
|
+
writeConfig(PRESETS.production);
|
|
210
|
+
const loaded = withNonTestEnv(() => loadConfig());
|
|
211
|
+
expect(loaded).toEqual(PRESETS.production);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
// ---------- getConfigFilePath ----------
|
|
215
|
+
describe('getConfigFilePath', () => {
|
|
216
|
+
it('returns path under ~/.tq/', () => {
|
|
217
|
+
const p = getConfigFilePath();
|
|
218
|
+
expect(p).toBe(path.join(state.tmpDir, '.tq', 'config.json'));
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
//# sourceMappingURL=config.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../src/config.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB,8DAA8D;AAC9D,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAEjD,uEAAuE;AACvE,EAAE,CAAC,IAAI,CAAC,kBAAkB,EAAE,GAAG,EAAE,CAAC,CAAC;IACjC,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC;IAC9C,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI;CACvB,CAAC,CAAC,CAAC;AAEJ,OAAO,EACL,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,OAAO,GAER,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,SAAS,cAAc,CAAI,EAAW;IACpC,MAAM,KAAK,GAAG;QACZ,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM;QAC1B,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ;QAC9B,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;KAC3C,CAAC;IACF,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;IAC1B,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,aAAa,CAAC;IACrC,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAClC,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;;YAC7D,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;QAC/B,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;YAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;;YACnE,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;QACjC,IAAI,KAAK,CAAC,cAAc,KAAK,SAAS;YACpC,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,KAAK,CAAC,cAAc,CAAC;;YAC/C,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACzC,CAAC;AACH,CAAC;AAED,UAAU,CAAC,GAAG,EAAE;IACd,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC;IACxE,0BAA0B;IAC1B,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAClC,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;AACxC,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACb,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAClC,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;AACxC,CAAC,CAAC,CAAC;AAEH,0CAA0C;AAE1C,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,MAAM,GAAG,iBAAiB,CAAC,sBAAsB,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QACnD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;QACrD,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,MAAM,GAAG,iBAAiB,CAAC,uBAAuB,CAAC,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,MAAM,GAAG,iBAAiB,CAAC,4BAA4B,CAAC,CAAC;QAC/D,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,qEAAqE;QACrE,gEAAgE;QAChE,qDAAqD;QACrD,MAAM,MAAM,GAAG,iBAAiB,CAAC,wBAAwB,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,MAAM,GAAG,iBAAiB,CAAC,sBAAsB,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC9C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,MAAM,GAAG,iBAAiB,CAAC,0BAA0B,CAAC,CAAC;QAC7D,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,MAAM,GAAG,iBAAiB,CAAC,gCAAgC,CAAC,CAAC;QACnE,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;QAC7D,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,GAAG,iBAAiB,CAAC,uBAAuB,CAAC,CAAC;QACrD,MAAM,CAAC,GAAG,iBAAiB,CAAC,uBAAuB,CAAC,CAAC;QACrD,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,mCAAmC;AAEnC,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,uBAAuB,CAAC;QACrD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC7C,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,EAC/B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAChC,CAAC;QAEF,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,uBAAuB,CAAC;QACrD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC7C,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,EAC/B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,CACnC,CAAC;QAEF,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC9C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC7C,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,EAC/B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,CACnC,CAAC;QAEF,oEAAoE;QACpE,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC7C,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,EAAE,aAAa,CAAC,CAAC;QAEjE,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,uBAAuB,CAAC;QACrD,OAAO,CAAC,GAAG,CAAC,kBAAkB,GAAG,IAAI,CAAC;QACtC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC7C,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,EAC/B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAC9B,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,kBAAkB,GAAG,IAAI,CAAC;QACtC,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,OAAO,CAAC,GAAG,CAAC,kBAAkB,GAAG,GAAG,CAAC;QACrC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC7C,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,EAC/B,IAAI,CAAC,SAAS,CAAC;YACb,WAAW,EAAE,OAAO;YACpB,MAAM,EAAE,uBAAuB;YAC/B,OAAO,EAAE,uBAAuB;YAChC,SAAS,EAAE,uBAAuB;SACnC,CAAC,CACH,CAAC;QACF,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,oCAAoC;AAEpC,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAE3B,MAAM,UAAU,GAAG,iBAAiB,EAAE,CAAC;QACvC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE7C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CACxB,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CACzB,CAAC;QACd,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC3B,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAE7B,MAAM,UAAU,GAAG,iBAAiB,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CACxB,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CACzB,CAAC;QACd,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAE3B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAC/C,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAChC,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,0CAA0C;AAE1C,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,GAAG,iBAAiB,EAAE,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process advisory file lock for coordinating token refresh.
|
|
3
|
+
*
|
|
4
|
+
* Problem: each MCP process has its own OAuthClient with a per-process
|
|
5
|
+
* refreshPromise dedup. When multiple MCP processes (tq-backender-proxy,
|
|
6
|
+
* tq-agent-bridge, etc.) hit an expired access token at roughly the same
|
|
7
|
+
* time, they ALL POST /v1/oauth/token with the same refresh token. The
|
|
8
|
+
* backend rotates the refresh token on first request; subsequent requests
|
|
9
|
+
* see invalid_grant (400). After CLEAR_TOKENS_AFTER_FAILURES (3)
|
|
10
|
+
* consecutive 400s, clearTokens() wipes the environment entry entirely.
|
|
11
|
+
*
|
|
12
|
+
* Fix: before calling the refresh endpoint, acquire an exclusive lock
|
|
13
|
+
* file keyed on apiBaseUrl. On acquire, re-read the token file — if
|
|
14
|
+
* another process refreshed while we waited, adopt their tokens and
|
|
15
|
+
* skip the network call. Otherwise, call the refresh endpoint and
|
|
16
|
+
* release the lock.
|
|
17
|
+
*
|
|
18
|
+
* Stale lock handling: if the lock file's mtime is older than
|
|
19
|
+
* STALE_LOCK_THRESHOLD_MS, consider the holder dead (crashed, killed)
|
|
20
|
+
* and break the lock. This keeps a single bad actor from wedging all
|
|
21
|
+
* MCP processes forever.
|
|
22
|
+
*/
|
|
23
|
+
export interface FileLockHandle {
|
|
24
|
+
release: () => void;
|
|
25
|
+
/** Was the lock broken because the previous holder left a stale file? */
|
|
26
|
+
brokeStaleLock: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a lock file path from an apiBaseUrl. Each environment
|
|
30
|
+
* (localhost, staging, production) gets its own lock since their
|
|
31
|
+
* tokens are independent.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getLockPath(apiBaseUrl: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Acquire an exclusive lock file. Blocks up to `maxWaitMs` while another
|
|
36
|
+
* holder has it. If the lock file is stale (mtime > threshold), break it
|
|
37
|
+
* and take it. Returns a handle with `release()` that MUST be called
|
|
38
|
+
* in finally.
|
|
39
|
+
*/
|
|
40
|
+
export declare function acquireFileLock(lockPath: string, maxWaitMs?: number): Promise<FileLockHandle>;
|
|
41
|
+
//# sourceMappingURL=file-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock.d.ts","sourceRoot":"","sources":["../src/file-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAWH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAItD;AAsBD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAS,GACjB,OAAO,CAAC,cAAc,CAAC,CAiDzB"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process advisory file lock for coordinating token refresh.
|
|
3
|
+
*
|
|
4
|
+
* Problem: each MCP process has its own OAuthClient with a per-process
|
|
5
|
+
* refreshPromise dedup. When multiple MCP processes (tq-backender-proxy,
|
|
6
|
+
* tq-agent-bridge, etc.) hit an expired access token at roughly the same
|
|
7
|
+
* time, they ALL POST /v1/oauth/token with the same refresh token. The
|
|
8
|
+
* backend rotates the refresh token on first request; subsequent requests
|
|
9
|
+
* see invalid_grant (400). After CLEAR_TOKENS_AFTER_FAILURES (3)
|
|
10
|
+
* consecutive 400s, clearTokens() wipes the environment entry entirely.
|
|
11
|
+
*
|
|
12
|
+
* Fix: before calling the refresh endpoint, acquire an exclusive lock
|
|
13
|
+
* file keyed on apiBaseUrl. On acquire, re-read the token file — if
|
|
14
|
+
* another process refreshed while we waited, adopt their tokens and
|
|
15
|
+
* skip the network call. Otherwise, call the refresh endpoint and
|
|
16
|
+
* release the lock.
|
|
17
|
+
*
|
|
18
|
+
* Stale lock handling: if the lock file's mtime is older than
|
|
19
|
+
* STALE_LOCK_THRESHOLD_MS, consider the holder dead (crashed, killed)
|
|
20
|
+
* and break the lock. This keeps a single bad actor from wedging all
|
|
21
|
+
* MCP processes forever.
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'fs';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
import { getTqDir } from './token-store.js';
|
|
26
|
+
const LOCK_DIR = 'locks';
|
|
27
|
+
const STALE_LOCK_THRESHOLD_MS = 30_000; // 30s — well over a normal refresh
|
|
28
|
+
const ACQUIRE_POLL_INTERVAL_MS = 50;
|
|
29
|
+
const ACQUIRE_JITTER_MS = 100;
|
|
30
|
+
/**
|
|
31
|
+
* Build a lock file path from an apiBaseUrl. Each environment
|
|
32
|
+
* (localhost, staging, production) gets its own lock since their
|
|
33
|
+
* tokens are independent.
|
|
34
|
+
*/
|
|
35
|
+
export function getLockPath(apiBaseUrl) {
|
|
36
|
+
const safe = apiBaseUrl.replace(/[^a-zA-Z0-9]/g, '_');
|
|
37
|
+
const dir = path.join(getTqDir(), LOCK_DIR);
|
|
38
|
+
return path.join(dir, `refresh-${safe}.lock`);
|
|
39
|
+
}
|
|
40
|
+
function ensureLockDir(lockPath) {
|
|
41
|
+
const dir = path.dirname(lockPath);
|
|
42
|
+
if (!fs.existsSync(dir)) {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function breakStaleLockIfAny(lockPath) {
|
|
47
|
+
try {
|
|
48
|
+
const stat = fs.statSync(lockPath);
|
|
49
|
+
if (Date.now() - stat.mtimeMs > STALE_LOCK_THRESHOLD_MS) {
|
|
50
|
+
fs.unlinkSync(lockPath);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// lock file gone — race with holder releasing; caller will retry
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Acquire an exclusive lock file. Blocks up to `maxWaitMs` while another
|
|
61
|
+
* holder has it. If the lock file is stale (mtime > threshold), break it
|
|
62
|
+
* and take it. Returns a handle with `release()` that MUST be called
|
|
63
|
+
* in finally.
|
|
64
|
+
*/
|
|
65
|
+
export async function acquireFileLock(lockPath, maxWaitMs = 10_000) {
|
|
66
|
+
ensureLockDir(lockPath);
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
let brokeStaleLock = false;
|
|
69
|
+
while (true) {
|
|
70
|
+
try {
|
|
71
|
+
// 'wx' — exclusive create, fail if exists (atomic on POSIX)
|
|
72
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
73
|
+
fs.writeSync(fd, `${process.pid}\n${Date.now()}`);
|
|
74
|
+
fs.closeSync(fd);
|
|
75
|
+
let released = false;
|
|
76
|
+
return {
|
|
77
|
+
brokeStaleLock,
|
|
78
|
+
release: () => {
|
|
79
|
+
if (released)
|
|
80
|
+
return;
|
|
81
|
+
released = true;
|
|
82
|
+
try {
|
|
83
|
+
fs.unlinkSync(lockPath);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Already gone — either we crashed and another process broke
|
|
87
|
+
// it, or a test cleaned up. Either way, benign.
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err.code !== 'EEXIST')
|
|
94
|
+
throw err;
|
|
95
|
+
if (breakStaleLockIfAny(lockPath)) {
|
|
96
|
+
brokeStaleLock = true;
|
|
97
|
+
// Retry immediately after breaking a stale lock
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (Date.now() - start >= maxWaitMs) {
|
|
101
|
+
throw new Error(`Could not acquire lock ${lockPath} within ${maxWaitMs}ms`);
|
|
102
|
+
}
|
|
103
|
+
// Wait with jitter to avoid thundering herd
|
|
104
|
+
const delay = ACQUIRE_POLL_INTERVAL_MS +
|
|
105
|
+
Math.floor(Math.random() * ACQUIRE_JITTER_MS);
|
|
106
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=file-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock.js","sourceRoot":"","sources":["../src/file-lock.ts"],"names":[],"mappings":"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"}
|