@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.
Files changed (70) hide show
  1. package/README.md +37 -0
  2. package/dist/config-watcher.d.ts +65 -0
  3. package/dist/config-watcher.d.ts.map +1 -0
  4. package/dist/config-watcher.js +140 -0
  5. package/dist/config-watcher.js.map +1 -0
  6. package/dist/config-watcher.test.d.ts +2 -0
  7. package/dist/config-watcher.test.d.ts.map +1 -0
  8. package/dist/config-watcher.test.js +116 -0
  9. package/dist/config-watcher.test.js.map +1 -0
  10. package/dist/config.d.ts +58 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +234 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/config.test.d.ts +2 -0
  15. package/dist/config.test.d.ts.map +1 -0
  16. package/dist/config.test.js +221 -0
  17. package/dist/config.test.js.map +1 -0
  18. package/dist/file-lock.d.ts +41 -0
  19. package/dist/file-lock.d.ts.map +1 -0
  20. package/dist/file-lock.js +110 -0
  21. package/dist/file-lock.js.map +1 -0
  22. package/dist/index.d.ts +43 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +43 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/oauth-client.d.ts +156 -0
  27. package/dist/oauth-client.d.ts.map +1 -0
  28. package/dist/oauth-client.js +572 -0
  29. package/dist/oauth-client.js.map +1 -0
  30. package/dist/oauth-client.test.d.ts +2 -0
  31. package/dist/oauth-client.test.d.ts.map +1 -0
  32. package/dist/oauth-client.test.js +290 -0
  33. package/dist/oauth-client.test.js.map +1 -0
  34. package/dist/test-utils.d.ts +10 -0
  35. package/dist/test-utils.d.ts.map +1 -0
  36. package/dist/test-utils.js +45 -0
  37. package/dist/test-utils.js.map +1 -0
  38. package/dist/token-store.d.ts +78 -0
  39. package/dist/token-store.d.ts.map +1 -0
  40. package/dist/token-store.js +299 -0
  41. package/dist/token-store.js.map +1 -0
  42. package/dist/token-store.test.d.ts +2 -0
  43. package/dist/token-store.test.d.ts.map +1 -0
  44. package/dist/token-store.test.js +239 -0
  45. package/dist/token-store.test.js.map +1 -0
  46. package/dist/token-watcher.d.ts +82 -0
  47. package/dist/token-watcher.d.ts.map +1 -0
  48. package/dist/token-watcher.js +185 -0
  49. package/dist/token-watcher.js.map +1 -0
  50. package/dist/types.d.ts +70 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +5 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/workspace-store.d.ts +60 -0
  55. package/dist/workspace-store.d.ts.map +1 -0
  56. package/dist/workspace-store.js +147 -0
  57. package/dist/workspace-store.js.map +1 -0
  58. package/dist/workspace-store.test.d.ts +2 -0
  59. package/dist/workspace-store.test.d.ts.map +1 -0
  60. package/dist/workspace-store.test.js +169 -0
  61. package/dist/workspace-store.test.js.map +1 -0
  62. package/dist/workspaces.d.ts +60 -0
  63. package/dist/workspaces.d.ts.map +1 -0
  64. package/dist/workspaces.js +80 -0
  65. package/dist/workspaces.js.map +1 -0
  66. package/dist/workspaces.test.d.ts +2 -0
  67. package/dist/workspaces.test.d.ts.map +1 -0
  68. package/dist/workspaces.test.js +111 -0
  69. package/dist/workspaces.test.js.map +1 -0
  70. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.test.d.ts.map
@@ -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":"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"}
@@ -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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"}