@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/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @levr-one/auth
|
|
2
|
+
|
|
3
|
+
Shared OAuth 2.1 client with self-healing token management, used by the Levr
|
|
4
|
+
CLIs and tooling. Provides a browser (PKCE) and device authorization flow, an
|
|
5
|
+
on-disk token/config store under `~/.tq/`, and a `listWorkspaces()` helper.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @levr-one/auth
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { OAuthClient, listWorkspaces } from '@levr-one/auth';
|
|
17
|
+
|
|
18
|
+
// Authenticate (opens a browser by default; use authorizeDevice() for headless).
|
|
19
|
+
const client = new OAuthClient({
|
|
20
|
+
clientId: 'my-client',
|
|
21
|
+
authServerUrl: 'https://api.levr.now',
|
|
22
|
+
});
|
|
23
|
+
await client.authorize();
|
|
24
|
+
|
|
25
|
+
// List the workspaces the signed-in user belongs to.
|
|
26
|
+
const workspaces = await listWorkspaces();
|
|
27
|
+
for (const w of workspaces) {
|
|
28
|
+
console.log(w.workspace_name, w.role, w.is_primary ? '(primary)' : '');
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`listWorkspaces()` throws a typed `WorkspaceFetchError` (never terminates the
|
|
33
|
+
process) on auth or HTTP errors — catch it and handle as your app sees fit.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigWatcher - Environment Change Detection
|
|
3
|
+
*
|
|
4
|
+
* Watches ~/.tq/config.json for changes and notifies subscribers.
|
|
5
|
+
* This enables MCP servers to auto-reconfigure when the user runs
|
|
6
|
+
* `yarn tq:env <preset>` without requiring a restart.
|
|
7
|
+
*/
|
|
8
|
+
import type { TqConfig } from './config.js';
|
|
9
|
+
export type ConfigChangeCallback = (config: TqConfig) => void;
|
|
10
|
+
export interface ConfigWatcherOptions {
|
|
11
|
+
/** Polling interval in milliseconds (backup if fs.watch misses events) */
|
|
12
|
+
pollInterval?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* ConfigWatcher for environment change detection
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const watcher = new ConfigWatcher();
|
|
20
|
+
* watcher.onConfigChange((config) => {
|
|
21
|
+
* console.log(`Environment changed to ${config.environment} (${config.apiUrl})`);
|
|
22
|
+
* client.setConfig({ baseUrl: config.apiUrl });
|
|
23
|
+
* });
|
|
24
|
+
* watcher.start();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class ConfigWatcher {
|
|
28
|
+
private watcher;
|
|
29
|
+
private pollTimer;
|
|
30
|
+
private lastConfigJson;
|
|
31
|
+
private lastConfig;
|
|
32
|
+
private callbacks;
|
|
33
|
+
private options;
|
|
34
|
+
private isRunning;
|
|
35
|
+
constructor(options?: ConfigWatcherOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Start watching for config changes
|
|
38
|
+
*/
|
|
39
|
+
start(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Stop watching
|
|
42
|
+
*/
|
|
43
|
+
stop(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Register a callback for config changes
|
|
46
|
+
*/
|
|
47
|
+
onConfigChange(callback: ConfigChangeCallback): void;
|
|
48
|
+
/**
|
|
49
|
+
* Remove a callback
|
|
50
|
+
*/
|
|
51
|
+
offConfigChange(callback: ConfigChangeCallback): void;
|
|
52
|
+
/**
|
|
53
|
+
* Get current config snapshot
|
|
54
|
+
*/
|
|
55
|
+
getCurrentConfig(): TqConfig | null;
|
|
56
|
+
/**
|
|
57
|
+
* Force a config check
|
|
58
|
+
*/
|
|
59
|
+
forceCheck(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Internal: Check config and notify if changed
|
|
62
|
+
*/
|
|
63
|
+
private checkConfig;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=config-watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-watcher.d.ts","sourceRoot":"","sources":["../src/config-watcher.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAMD;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,SAAS,CAAwC;IACzD,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,SAAS,CAAkB;gBAEvB,OAAO,CAAC,EAAE,oBAAoB;IAI1C;;OAEG;IACH,KAAK,IAAI,IAAI;IA4Cb;;OAEG;IACH,IAAI,IAAI,IAAI;IAcZ;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAIpD;;OAEG;IACH,eAAe,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAIrD;;OAEG;IACH,gBAAgB,IAAI,QAAQ,GAAG,IAAI;IAInC;;OAEG;IACH,UAAU,IAAI,IAAI;IAIlB;;OAEG;IACH,OAAO,CAAC,WAAW;CAsBpB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigWatcher - Environment Change Detection
|
|
3
|
+
*
|
|
4
|
+
* Watches ~/.tq/config.json for changes and notifies subscribers.
|
|
5
|
+
* This enables MCP servers to auto-reconfigure when the user runs
|
|
6
|
+
* `yarn tq:env <preset>` without requiring a restart.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { loadConfig, getConfigFilePath } from './config.js';
|
|
11
|
+
import { getTqDir } from './token-store.js';
|
|
12
|
+
const DEFAULT_OPTIONS = {
|
|
13
|
+
pollInterval: 10000, // 10 seconds
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* ConfigWatcher for environment change detection
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const watcher = new ConfigWatcher();
|
|
21
|
+
* watcher.onConfigChange((config) => {
|
|
22
|
+
* console.log(`Environment changed to ${config.environment} (${config.apiUrl})`);
|
|
23
|
+
* client.setConfig({ baseUrl: config.apiUrl });
|
|
24
|
+
* });
|
|
25
|
+
* watcher.start();
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class ConfigWatcher {
|
|
29
|
+
watcher = null;
|
|
30
|
+
pollTimer = null;
|
|
31
|
+
lastConfigJson = '';
|
|
32
|
+
lastConfig = null;
|
|
33
|
+
callbacks = new Set();
|
|
34
|
+
options;
|
|
35
|
+
isRunning = false;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Start watching for config changes
|
|
41
|
+
*/
|
|
42
|
+
start() {
|
|
43
|
+
if (this.isRunning)
|
|
44
|
+
return;
|
|
45
|
+
this.isRunning = true;
|
|
46
|
+
const tqDir = getTqDir();
|
|
47
|
+
// Ensure directory exists for watching
|
|
48
|
+
if (!fs.existsSync(tqDir)) {
|
|
49
|
+
try {
|
|
50
|
+
fs.mkdirSync(tqDir, { recursive: true, mode: 0o700 });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
console.error('[tq-oauth] Failed to create .tq directory for config watching');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Watch the directory (file may not exist yet)
|
|
57
|
+
const configFileName = path.basename(getConfigFilePath());
|
|
58
|
+
try {
|
|
59
|
+
this.watcher = fs.watch(tqDir, (_eventType, filename) => {
|
|
60
|
+
if (filename === configFileName) {
|
|
61
|
+
this.checkConfig();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
this.watcher.on('error', (error) => {
|
|
65
|
+
console.error('[tq-oauth] Config watch error:', error);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error('[tq-oauth] Failed to start config watcher:', error);
|
|
70
|
+
}
|
|
71
|
+
// Poll as backup (unref so timer doesn't keep the process alive)
|
|
72
|
+
this.pollTimer = setInterval(() => {
|
|
73
|
+
this.checkConfig();
|
|
74
|
+
}, this.options.pollInterval);
|
|
75
|
+
this.pollTimer.unref();
|
|
76
|
+
// Initial snapshot (don't notify — caller already has initial config)
|
|
77
|
+
this.lastConfig = loadConfig();
|
|
78
|
+
this.lastConfigJson = JSON.stringify(this.lastConfig);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stop watching
|
|
82
|
+
*/
|
|
83
|
+
stop() {
|
|
84
|
+
this.isRunning = false;
|
|
85
|
+
if (this.watcher) {
|
|
86
|
+
this.watcher.close();
|
|
87
|
+
this.watcher = null;
|
|
88
|
+
}
|
|
89
|
+
if (this.pollTimer) {
|
|
90
|
+
clearInterval(this.pollTimer);
|
|
91
|
+
this.pollTimer = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Register a callback for config changes
|
|
96
|
+
*/
|
|
97
|
+
onConfigChange(callback) {
|
|
98
|
+
this.callbacks.add(callback);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Remove a callback
|
|
102
|
+
*/
|
|
103
|
+
offConfigChange(callback) {
|
|
104
|
+
this.callbacks.delete(callback);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get current config snapshot
|
|
108
|
+
*/
|
|
109
|
+
getCurrentConfig() {
|
|
110
|
+
return this.lastConfig;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Force a config check
|
|
114
|
+
*/
|
|
115
|
+
forceCheck() {
|
|
116
|
+
this.checkConfig();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Internal: Check config and notify if changed
|
|
120
|
+
*/
|
|
121
|
+
checkConfig() {
|
|
122
|
+
const config = loadConfig();
|
|
123
|
+
const configJson = JSON.stringify(config);
|
|
124
|
+
if (configJson !== this.lastConfigJson) {
|
|
125
|
+
const prev = this.lastConfig;
|
|
126
|
+
this.lastConfig = config;
|
|
127
|
+
this.lastConfigJson = configJson;
|
|
128
|
+
console.error(`[tq-oauth] Config change detected: ${prev?.environment ?? 'unknown'} (${prev?.apiUrl ?? '?'}) → ${config.environment} (${config.apiUrl})`);
|
|
129
|
+
this.callbacks.forEach((cb) => {
|
|
130
|
+
try {
|
|
131
|
+
cb(config);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.error('[tq-oauth] Error in config change callback:', error);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=config-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-watcher.js","sourceRoot":"","sources":["../src/config-watcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-watcher.test.d.ts","sourceRoot":"","sources":["../src/config-watcher.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
const state = vi.hoisted(() => ({ tmpDir: '' }));
|
|
6
|
+
vi.mock('os', async (importOriginal) => {
|
|
7
|
+
const original = await importOriginal();
|
|
8
|
+
return { ...original, homedir: () => state.tmpDir };
|
|
9
|
+
});
|
|
10
|
+
import { withNonTestEnv } from './test-utils.js';
|
|
11
|
+
let ConfigWatcher;
|
|
12
|
+
let writeConfig;
|
|
13
|
+
let PRESETS;
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
state.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tq-cfg-watch-'));
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
const configMod = await import('./config.js');
|
|
18
|
+
writeConfig = configMod.writeConfig;
|
|
19
|
+
PRESETS = configMod.PRESETS;
|
|
20
|
+
const watcherMod = await import('./config-watcher.js');
|
|
21
|
+
ConfigWatcher = watcherMod.ConfigWatcher;
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
fs.rmSync(state.tmpDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
describe('ConfigWatcher', () => {
|
|
27
|
+
it('starts and stops without error', () => {
|
|
28
|
+
const watcher = new ConfigWatcher();
|
|
29
|
+
watcher.start();
|
|
30
|
+
watcher.stop();
|
|
31
|
+
});
|
|
32
|
+
it('does not notify on start (initial snapshot only)', () => {
|
|
33
|
+
writeConfig(PRESETS.staging);
|
|
34
|
+
const watcher = new ConfigWatcher();
|
|
35
|
+
const cb = vi.fn();
|
|
36
|
+
watcher.onConfigChange(cb);
|
|
37
|
+
watcher.start();
|
|
38
|
+
expect(cb).not.toHaveBeenCalled();
|
|
39
|
+
watcher.stop();
|
|
40
|
+
});
|
|
41
|
+
it('notifies when config changes via forceCheck', () => {
|
|
42
|
+
withNonTestEnv(() => {
|
|
43
|
+
writeConfig(PRESETS.staging);
|
|
44
|
+
const watcher = new ConfigWatcher();
|
|
45
|
+
const cb = vi.fn();
|
|
46
|
+
watcher.onConfigChange(cb);
|
|
47
|
+
watcher.start();
|
|
48
|
+
// Change config
|
|
49
|
+
writeConfig(PRESETS.local);
|
|
50
|
+
watcher.forceCheck();
|
|
51
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
52
|
+
const changed = cb.mock.calls[0][0];
|
|
53
|
+
expect(changed.environment).toBe('local');
|
|
54
|
+
// Match what was written. PRESETS.local is a getter that returns
|
|
55
|
+
// the HTTPS variant when DEV_TLS dev mode is detected, so this
|
|
56
|
+
// assertion stays correct in both modes.
|
|
57
|
+
expect(changed.apiUrl).toBe(PRESETS.local.apiUrl);
|
|
58
|
+
watcher.stop();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
it('does not notify when config is unchanged', () => {
|
|
62
|
+
writeConfig(PRESETS.staging);
|
|
63
|
+
const watcher = new ConfigWatcher();
|
|
64
|
+
const cb = vi.fn();
|
|
65
|
+
watcher.onConfigChange(cb);
|
|
66
|
+
watcher.start();
|
|
67
|
+
watcher.forceCheck();
|
|
68
|
+
expect(cb).not.toHaveBeenCalled();
|
|
69
|
+
watcher.stop();
|
|
70
|
+
});
|
|
71
|
+
it('getCurrentConfig returns latest config', () => {
|
|
72
|
+
withNonTestEnv(() => {
|
|
73
|
+
writeConfig(PRESETS.production);
|
|
74
|
+
const watcher = new ConfigWatcher();
|
|
75
|
+
watcher.start();
|
|
76
|
+
expect(watcher.getCurrentConfig()?.environment).toBe('production');
|
|
77
|
+
writeConfig(PRESETS.local);
|
|
78
|
+
watcher.forceCheck();
|
|
79
|
+
expect(watcher.getCurrentConfig()?.environment).toBe('local');
|
|
80
|
+
watcher.stop();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
it('offConfigChange removes callback', () => {
|
|
84
|
+
withNonTestEnv(() => {
|
|
85
|
+
writeConfig(PRESETS.staging);
|
|
86
|
+
const watcher = new ConfigWatcher();
|
|
87
|
+
const cb = vi.fn();
|
|
88
|
+
watcher.onConfigChange(cb);
|
|
89
|
+
watcher.offConfigChange(cb);
|
|
90
|
+
watcher.start();
|
|
91
|
+
writeConfig(PRESETS.local);
|
|
92
|
+
watcher.forceCheck();
|
|
93
|
+
expect(cb).not.toHaveBeenCalled();
|
|
94
|
+
watcher.stop();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
it('handles callback errors without crashing', () => {
|
|
98
|
+
withNonTestEnv(() => {
|
|
99
|
+
writeConfig(PRESETS.staging);
|
|
100
|
+
const watcher = new ConfigWatcher();
|
|
101
|
+
const badCb = vi.fn(() => {
|
|
102
|
+
throw new Error('callback error');
|
|
103
|
+
});
|
|
104
|
+
const goodCb = vi.fn();
|
|
105
|
+
watcher.onConfigChange(badCb);
|
|
106
|
+
watcher.onConfigChange(goodCb);
|
|
107
|
+
watcher.start();
|
|
108
|
+
writeConfig(PRESETS.local);
|
|
109
|
+
watcher.forceCheck();
|
|
110
|
+
expect(badCb).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(goodCb).toHaveBeenCalledTimes(1);
|
|
112
|
+
watcher.stop();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=config-watcher.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-watcher.test.js","sourceRoot":"","sources":["../src/config-watcher.test.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
export interface TqConfig {
|
|
12
|
+
environment: 'local' | 'staging' | 'production' | 'custom';
|
|
13
|
+
apiUrl: string;
|
|
14
|
+
authUrl: string;
|
|
15
|
+
clientUrl: string;
|
|
16
|
+
oauthClientId: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Environment presets. Staging is the default.
|
|
20
|
+
*
|
|
21
|
+
* `local` is a getter that returns the HTTPS variant when DEV_TLS dev mode
|
|
22
|
+
* is detected (see `isLocalTlsMode()`), so `yarn tq:env local` writes the
|
|
23
|
+
* right URLs whether the user has TLS dev mode set up or not.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PRESETS: Record<'local' | 'staging' | 'production', TqConfig>;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a full TqConfig from a bare API URL by matching presets.
|
|
28
|
+
* Returns 'custom' environment if no preset matches.
|
|
29
|
+
*
|
|
30
|
+
* Local resolution covers three forms:
|
|
31
|
+
* - `http://localhost:8080` → LOCAL_HTTP
|
|
32
|
+
* - `https://api.levr.test:8080` → LOCAL_HTTPS (canonical TLS form)
|
|
33
|
+
* - `https://localhost:8080` → LOCAL_HTTPS (legacy alias)
|
|
34
|
+
*
|
|
35
|
+
* Resolving the legacy localhost alias to the new levr.test preset means
|
|
36
|
+
* a token issued in TLS mode under the old localhost URL still maps to
|
|
37
|
+
* the right config when read after this change.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveFromApiUrl(url: string): TqConfig;
|
|
40
|
+
/**
|
|
41
|
+
* Load unified config with precedence:
|
|
42
|
+
* 1. TQ_BACKEND_URL env var
|
|
43
|
+
* 2. ~/.tq/config.json (skipped in test environments)
|
|
44
|
+
* 3. Token's apiBaseUrl from ~/.tq/oauth-tokens.json (skipped in test environments)
|
|
45
|
+
* 4. Staging default
|
|
46
|
+
*
|
|
47
|
+
* TQ_OAUTH_CLIENT_ID env var overrides oauthClientId from any source.
|
|
48
|
+
*/
|
|
49
|
+
export declare function loadConfig(): TqConfig;
|
|
50
|
+
/**
|
|
51
|
+
* Write config to ~/.tq/config.json using atomic write (tmp + rename)
|
|
52
|
+
*/
|
|
53
|
+
export declare function writeConfig(config: TqConfig): void;
|
|
54
|
+
/**
|
|
55
|
+
* Get the config file path (~/.tq/config.json)
|
|
56
|
+
*/
|
|
57
|
+
export declare function getConfigFilePath(): string;
|
|
58
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAgED;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,GAAG,YAAY,EAAE,QAAQ,CAqBxE,CAAC;AAeF;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAqBvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,IAAI,QAAQ,CA+DrC;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAclD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|