mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(cli): load extra headers from settings (#1041)
* test: reproduce issue #786 * fix: load extra headers from settings (closes #786) * test: cover extra header precedence and redaction * fix: redact persisted extra headers in diagnostics * test: cover runner extra header identity * fix: restart runner when extra headers change
This commit is contained in:
+1
-1
@@ -100,7 +100,7 @@ See `src/configuration.ts` for all options.
|
||||
|
||||
- `HAPI_HOME` - Config/data directory (default: ~/.hapi).
|
||||
- `HAPI_EXPERIMENTAL` - Enable experimental features (true/1/yes).
|
||||
- `HAPI_EXTRA_HEADERS_JSON` - JSON object of extra headers to send on CLI → hub requests, e.g. `{"Cookie":"CF_Authorization=..."}`.
|
||||
- `HAPI_EXTRA_HEADERS_JSON` - JSON object of extra headers to send on CLI → hub requests, e.g. `{"Cookie":"CF_Authorization=..."}`. Can also be set as the `extraHeaders` object in `~/.hapi/settings.json` (environment variable wins).
|
||||
- `HAPI_CLAUDE_PATH` - Path to a specific `claude` executable.
|
||||
- `HAPI_HTTP_MCP_URL` - Default MCP target for `hapi mcp`.
|
||||
|
||||
|
||||
+23
-15
@@ -11,6 +11,28 @@ import { join } from 'node:path'
|
||||
import packageJson from '../package.json'
|
||||
import { getCliArgs } from '@/utils/cliArgs'
|
||||
|
||||
export function normalizeExtraHeaders(
|
||||
value: unknown,
|
||||
source: string,
|
||||
warn: (message: string) => void = console.warn
|
||||
): Record<string, string> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
warn(`[WARN] ${source} must be a JSON object. Ignoring value.`)
|
||||
return {}
|
||||
}
|
||||
|
||||
const entries = Object.entries(value)
|
||||
const headers = Object.fromEntries(
|
||||
entries.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
||||
)
|
||||
|
||||
if (Object.keys(headers).length !== entries.length) {
|
||||
warn(`[WARN] ${source} only supports string header values. Ignoring non-string entries.`)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export function parseExtraHeaders(raw: string | undefined, warn: (message: string) => void = console.warn): Record<string, string> {
|
||||
if (!raw) {
|
||||
return {}
|
||||
@@ -18,21 +40,7 @@ export function parseExtraHeaders(raw: string | undefined, warn: (message: strin
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
warn('[WARN] HAPI_EXTRA_HEADERS_JSON must be a JSON object. Ignoring value.')
|
||||
return {}
|
||||
}
|
||||
|
||||
const entries = Object.entries(parsed)
|
||||
const headers = Object.fromEntries(
|
||||
entries.filter((entry): entry is [string, string] => typeof entry[0] === 'string' && typeof entry[1] === 'string')
|
||||
)
|
||||
|
||||
if (Object.keys(headers).length !== entries.length) {
|
||||
warn('[WARN] HAPI_EXTRA_HEADERS_JSON only supports string header values. Ignoring non-string entries.')
|
||||
}
|
||||
|
||||
return headers
|
||||
return normalizeExtraHeaders(parsed, 'HAPI_EXTRA_HEADERS_JSON', warn)
|
||||
} catch {
|
||||
warn('[WARN] Failed to parse HAPI_EXTRA_HEADERS_JSON. Ignoring value.')
|
||||
return {}
|
||||
|
||||
@@ -19,6 +19,8 @@ interface Settings {
|
||||
cliApiToken?: string
|
||||
// API URL for server connections (priority: env HAPI_API_URL > this > default)
|
||||
apiUrl?: string
|
||||
// Extra headers for CLI -> hub requests (priority: env HAPI_EXTRA_HEADERS_JSON > this)
|
||||
extraHeaders?: unknown
|
||||
// Legacy field name (for migration, read-only)
|
||||
serverUrl?: string
|
||||
}
|
||||
@@ -38,6 +40,8 @@ export interface RunnerLocallyPersistedState {
|
||||
startedWithApiUrl?: string;
|
||||
startedWithMachineId?: string;
|
||||
startedWithCliApiTokenHash?: string;
|
||||
// SHA-256 of canonicalized extra headers. Raw header values must never be persisted here.
|
||||
startedWithExtraHeadersHash?: string;
|
||||
/**
|
||||
* Original process.argv.slice(2) of the runner process at start time, e.g.
|
||||
* ['runner', 'start-sync', '--workspace-root', '/home/user/code'].
|
||||
|
||||
@@ -12,7 +12,7 @@ import { join } from 'node:path';
|
||||
import { isBunCompiled, projectPath } from '@/projectPath';
|
||||
import { isProcessAlive, isHapiRunnerProcess, killProcess } from '@/utils/process';
|
||||
import { configuration } from '@/configuration';
|
||||
import { hashRunnerCliApiToken, isRunnerStateCompatibleWithIdentity } from './runnerIdentity';
|
||||
import { hashRunnerCliApiToken, hashRunnerExtraHeaders, isRunnerStateCompatibleWithIdentity } from './runnerIdentity';
|
||||
|
||||
export function getInstalledCliMtimeMs(): number | undefined {
|
||||
if (isBunCompiled()) {
|
||||
@@ -219,7 +219,8 @@ export async function isRunnerRunningCurrentlyInstalledHappyVersion(): Promise<b
|
||||
const currentIdentityMatches = isRunnerStateCompatibleWithIdentity(state, {
|
||||
apiUrl: currentApiUrl,
|
||||
machineId: currentMachineId,
|
||||
cliApiTokenHash: hashRunnerCliApiToken(currentCliApiToken)
|
||||
cliApiTokenHash: hashRunnerCliApiToken(currentCliApiToken),
|
||||
extraHeadersHash: hashRunnerExtraHeaders(configuration.extraHeaders)
|
||||
});
|
||||
logger.debug(`[RUNNER CONTROL] Runner identity match: ${currentIdentityMatches}`, {
|
||||
currentApiUrl,
|
||||
|
||||
@@ -25,7 +25,7 @@ import { validateWorkspaceDirectory } from './validateWorkspaceDirectory';
|
||||
import { join } from 'path';
|
||||
import { buildMachineMetadata } from '@/agent/sessionFactory';
|
||||
import { resolveWorkspaceRoots } from '@/utils/workspaceRoot';
|
||||
import { hashRunnerCliApiToken } from './runnerIdentity';
|
||||
import { hashRunnerCliApiToken, hashRunnerExtraHeaders } from './runnerIdentity';
|
||||
import { scheduleCursorModelsPrewarm } from '@/modules/common/cursorModelsPrewarm';
|
||||
|
||||
export async function startRunner(options: { workspaceRoots?: string[] } = {}): Promise<void> {
|
||||
@@ -744,6 +744,7 @@ export async function startRunner(options: { workspaceRoots?: string[] } = {}):
|
||||
startedWithApiUrl: configuration.apiUrl,
|
||||
startedWithMachineId: machineId,
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken(configuration.cliApiToken),
|
||||
startedWithExtraHeadersHash: hashRunnerExtraHeaders(configuration.extraHeaders),
|
||||
startedWithArgv,
|
||||
startedWithVersionHandoffDisabled,
|
||||
runnerLogPath: logger.logFilePath
|
||||
@@ -1021,6 +1022,7 @@ export async function startRunner(options: { workspaceRoots?: string[] } = {}):
|
||||
startedWithApiUrl: fileState.startedWithApiUrl,
|
||||
startedWithMachineId: fileState.startedWithMachineId,
|
||||
startedWithCliApiTokenHash: fileState.startedWithCliApiTokenHash,
|
||||
startedWithExtraHeadersHash: fileState.startedWithExtraHeadersHash,
|
||||
startedWithArgv,
|
||||
startedWithVersionHandoffDisabled,
|
||||
lastHeartbeat: new Date().toLocaleString(),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { hashRunnerCliApiToken, isRunnerStateCompatibleWithIdentity } from './runnerIdentity'
|
||||
import {
|
||||
hashRunnerCliApiToken,
|
||||
hashRunnerExtraHeaders,
|
||||
isRunnerStateCompatibleWithIdentity
|
||||
} from './runnerIdentity'
|
||||
|
||||
describe('runnerIdentity', () => {
|
||||
it('matches when api url, machine id, token hash all same', () => {
|
||||
@@ -49,6 +53,113 @@ describe('runnerIdentity', () => {
|
||||
)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects reused runner when extra headers changed', () => {
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
startedWithExtraHeadersHash: 'old-headers-hash'
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: 'new-headers-hash'
|
||||
}
|
||||
)).toBe(false)
|
||||
})
|
||||
|
||||
it('hashes equivalent extra headers identically regardless of insertion order', () => {
|
||||
const first = hashRunnerExtraHeaders({
|
||||
'X-Second': 'two',
|
||||
'X-First': 'one'
|
||||
})
|
||||
const second = hashRunnerExtraHeaders({
|
||||
'X-First': 'one',
|
||||
'X-Second': 'two'
|
||||
})
|
||||
|
||||
expect(first).toBe(second)
|
||||
expect(first).toMatch(/^[a-f0-9]{64}$/)
|
||||
})
|
||||
|
||||
it('matches equivalent extra headers with different insertion order', () => {
|
||||
const first = hashRunnerExtraHeaders({
|
||||
'X-Second': 'two',
|
||||
'X-First': 'one'
|
||||
})
|
||||
const second = hashRunnerExtraHeaders({
|
||||
'X-First': 'one',
|
||||
'X-Second': 'two'
|
||||
})
|
||||
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
startedWithExtraHeadersHash: first
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: second
|
||||
}
|
||||
)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps legacy runner state compatible when no extra headers are configured', () => {
|
||||
expect(hashRunnerExtraHeaders({})).toBeUndefined()
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token')
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: undefined
|
||||
}
|
||||
)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects legacy runner state when extra headers are now configured', () => {
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token')
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: 'configured-headers-hash'
|
||||
}
|
||||
)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects runner state with headers when current headers are empty', () => {
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
startedWithApiUrl: 'http://example.com',
|
||||
startedWithMachineId: 'machine-123',
|
||||
startedWithCliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
startedWithExtraHeadersHash: 'configured-headers-hash'
|
||||
},
|
||||
{
|
||||
apiUrl: 'http://example.com',
|
||||
machineId: 'machine-123',
|
||||
cliApiTokenHash: hashRunnerCliApiToken('secret-token'),
|
||||
extraHeadersHash: undefined
|
||||
}
|
||||
)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects reused runner when current machine id is missing', () => {
|
||||
expect(isRunnerStateCompatibleWithIdentity(
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ export type RunnerConnectionIdentity = {
|
||||
apiUrl: string
|
||||
machineId?: string
|
||||
cliApiTokenHash?: string
|
||||
extraHeadersHash?: string
|
||||
}
|
||||
|
||||
export function hashRunnerCliApiToken(token: string | null | undefined): string | undefined {
|
||||
@@ -15,10 +16,25 @@ export function hashRunnerCliApiToken(token: string | null | undefined): string
|
||||
return createHash('sha256').update(trimmed).digest('hex')
|
||||
}
|
||||
|
||||
export function hashRunnerExtraHeaders(
|
||||
headers: Readonly<Record<string, string>> | null | undefined
|
||||
): string | undefined {
|
||||
const entries = Object.entries(headers ?? {})
|
||||
if (entries.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
||||
return createHash('sha256').update(JSON.stringify(entries)).digest('hex')
|
||||
}
|
||||
|
||||
export function isRunnerStateCompatibleWithIdentity(
|
||||
state: Pick<
|
||||
RunnerLocallyPersistedState,
|
||||
'startedWithApiUrl' | 'startedWithMachineId' | 'startedWithCliApiTokenHash'
|
||||
| 'startedWithApiUrl'
|
||||
| 'startedWithMachineId'
|
||||
| 'startedWithCliApiTokenHash'
|
||||
| 'startedWithExtraHeadersHash'
|
||||
>,
|
||||
current: RunnerConnectionIdentity
|
||||
): boolean {
|
||||
@@ -34,5 +50,9 @@ export function isRunnerStateCompatibleWithIdentity(
|
||||
return false
|
||||
}
|
||||
|
||||
if (state.startedWithExtraHeadersHash !== current.extraHeadersHash) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { redactSettingsForDisplay } from './doctor'
|
||||
|
||||
describe('redactSettingsForDisplay', () => {
|
||||
it('redacts tokens and extra headers from diagnostic output', () => {
|
||||
const displaySettings = redactSettingsForDisplay({
|
||||
apiUrl: 'https://hapi.example.com',
|
||||
cliApiToken: 'cli-secret',
|
||||
extraHeaders: {
|
||||
'CF-Access-Client-Id': 'client-id',
|
||||
'CF-Access-Client-Secret': 'client-secret'
|
||||
}
|
||||
})
|
||||
|
||||
expect(displaySettings).toEqual({
|
||||
apiUrl: 'https://hapi.example.com',
|
||||
cliApiToken: '***',
|
||||
extraHeaders: '***'
|
||||
})
|
||||
expect(JSON.stringify(displaySettings)).not.toContain('cli-secret')
|
||||
expect(JSON.stringify(displaySettings)).not.toContain('client-secret')
|
||||
})
|
||||
})
|
||||
@@ -47,6 +47,14 @@ export function getEnvironmentInfo(): Record<string, any> {
|
||||
};
|
||||
}
|
||||
|
||||
export function redactSettingsForDisplay(settings: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
...settings,
|
||||
cliApiToken: settings.cliApiToken ? '***' : undefined,
|
||||
extraHeaders: settings.extraHeaders === undefined ? undefined : '***'
|
||||
}
|
||||
}
|
||||
|
||||
function getLogFiles(logDir: string): { file: string, path: string, modified: Date }[] {
|
||||
if (!existsSync(logDir)) {
|
||||
return [];
|
||||
@@ -126,8 +134,7 @@ export async function runDoctorCommand(filter?: 'all' | 'runner'): Promise<void>
|
||||
try {
|
||||
settings = await readSettings();
|
||||
console.log(chalk.bold('\n📄 Settings (settings.json):'));
|
||||
// Hide cliApiToken in output for security
|
||||
const displaySettings = { ...settings, cliApiToken: settings.cliApiToken ? '***' : undefined };
|
||||
const displaySettings = redactSettingsForDisplay({ ...settings });
|
||||
console.log(chalk.gray(JSON.stringify(displaySettings, null, 2)));
|
||||
} catch (error) {
|
||||
console.log(chalk.bold('\n📄 Settings:'));
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { configuration, normalizeExtraHeaders } from '@/configuration'
|
||||
import { readSettings } from '@/persistence'
|
||||
|
||||
export async function initializeExtraHeaders(): Promise<void> {
|
||||
if (process.env.HAPI_EXTRA_HEADERS_JSON !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const settings = await readSettings()
|
||||
if (settings.extraHeaders === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
configuration._setExtraHeaders(
|
||||
normalizeExtraHeaders(settings.extraHeaders, 'settings.json extraHeaders')
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { initializeApiUrlMock, readSettingsMock, updateSettingsMock } = vi.hoisted(() => ({
|
||||
initializeApiUrlMock: vi.fn(async () => {}),
|
||||
readSettingsMock: vi.fn(),
|
||||
updateSettingsMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/ui/apiUrlInit', () => ({
|
||||
initializeApiUrl: initializeApiUrlMock
|
||||
}))
|
||||
|
||||
vi.mock('@/persistence', () => ({
|
||||
readSettings: readSettingsMock,
|
||||
updateSettings: updateSettingsMock
|
||||
}))
|
||||
|
||||
import { configuration } from '@/configuration'
|
||||
import { initializeToken } from './tokenInit'
|
||||
|
||||
describe('initializeToken extra headers', () => {
|
||||
const originalExtraHeadersEnv = process.env.HAPI_EXTRA_HEADERS_JSON
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.HAPI_EXTRA_HEADERS_JSON
|
||||
configuration._setCliApiToken('token-from-env')
|
||||
configuration._setExtraHeaders({})
|
||||
initializeApiUrlMock.mockClear()
|
||||
readSettingsMock.mockReset()
|
||||
updateSettingsMock.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalExtraHeadersEnv === undefined) {
|
||||
delete process.env.HAPI_EXTRA_HEADERS_JSON
|
||||
} else {
|
||||
process.env.HAPI_EXTRA_HEADERS_JSON = originalExtraHeadersEnv
|
||||
}
|
||||
configuration._setExtraHeaders({})
|
||||
})
|
||||
|
||||
it('loads extra headers from settings even when the token is already initialized', async () => {
|
||||
readSettingsMock.mockResolvedValue({
|
||||
extraHeaders: {
|
||||
'CF-Access-Client-Id': 'client-id',
|
||||
'CF-Access-Client-Secret': 'client-secret'
|
||||
}
|
||||
})
|
||||
|
||||
await initializeToken()
|
||||
|
||||
expect(configuration.extraHeaders).toEqual({
|
||||
'CF-Access-Client-Id': 'client-id',
|
||||
'CF-Access-Client-Secret': 'client-secret'
|
||||
})
|
||||
})
|
||||
|
||||
it('loads both the token and extra headers from settings', async () => {
|
||||
configuration._setCliApiToken('')
|
||||
readSettingsMock.mockResolvedValue({
|
||||
cliApiToken: 'token-from-settings',
|
||||
extraHeaders: {
|
||||
Cookie: 'CF_Authorization=from-settings'
|
||||
}
|
||||
})
|
||||
|
||||
await initializeToken()
|
||||
|
||||
expect(configuration.cliApiToken).toBe('token-from-settings')
|
||||
expect(configuration.extraHeaders).toEqual({
|
||||
Cookie: 'CF_Authorization=from-settings'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps environment extra headers instead of loading settings', async () => {
|
||||
process.env.HAPI_EXTRA_HEADERS_JSON = '{"X-Source":"environment"}'
|
||||
configuration._setExtraHeaders({ 'X-Source': 'environment' })
|
||||
readSettingsMock.mockResolvedValue({
|
||||
extraHeaders: { 'X-Source': 'settings' }
|
||||
})
|
||||
|
||||
await initializeToken()
|
||||
|
||||
expect(configuration.extraHeaders).toEqual({ 'X-Source': 'environment' })
|
||||
expect(readSettingsMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['{}', '{not-json'])(
|
||||
'does not fall back to settings when the environment value is %s',
|
||||
async (environmentValue) => {
|
||||
process.env.HAPI_EXTRA_HEADERS_JSON = environmentValue
|
||||
configuration._setExtraHeaders({})
|
||||
readSettingsMock.mockResolvedValue({
|
||||
extraHeaders: { Cookie: 'CF_Authorization=from-settings' }
|
||||
})
|
||||
|
||||
await initializeToken()
|
||||
|
||||
expect(configuration.extraHeaders).toEqual({})
|
||||
expect(readSettingsMock).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('drops non-string settings header values without exposing them', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
readSettingsMock.mockResolvedValue({
|
||||
extraHeaders: {
|
||||
Cookie: 'CF_Authorization=valid',
|
||||
'X-Numeric-Secret': 12345,
|
||||
'X-Boolean-Secret': true
|
||||
}
|
||||
})
|
||||
|
||||
await initializeToken()
|
||||
|
||||
expect(configuration.extraHeaders).toEqual({
|
||||
Cookie: 'CF_Authorization=valid'
|
||||
})
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[WARN] settings.json extraHeaders only supports string header values. Ignoring non-string entries.'
|
||||
)
|
||||
expect(JSON.stringify(warnSpy.mock.calls)).not.toContain('12345')
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import chalk from 'chalk'
|
||||
import { configuration } from '@/configuration'
|
||||
import { readSettings, updateSettings } from '@/persistence'
|
||||
import { initializeApiUrl } from '@/ui/apiUrlInit'
|
||||
import { initializeExtraHeaders } from '@/ui/extraHeadersInit'
|
||||
|
||||
/**
|
||||
* Initialize CLI API token
|
||||
@@ -21,6 +22,7 @@ import { initializeApiUrl } from '@/ui/apiUrlInit'
|
||||
export async function initializeToken(): Promise<void> {
|
||||
// Initialize API URL first (env > settings.json > default)
|
||||
await initializeApiUrl()
|
||||
await initializeExtraHeaders()
|
||||
|
||||
// 1. Environment variable has highest priority (allows temporary override)
|
||||
if (configuration.cliApiToken) {
|
||||
|
||||
Reference in New Issue
Block a user