@gwdn/nexus-mcp 0.8.4 → 0.8.6
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/dist/machine-id.d.ts +19 -0
- package/dist/machine-id.d.ts.map +1 -0
- package/dist/machine-id.js +52 -0
- package/dist/machine-id.js.map +1 -0
- package/dist/nexus-api.d.ts +1 -0
- package/dist/nexus-api.d.ts.map +1 -1
- package/dist/nexus-api.js +12 -0
- package/dist/nexus-api.js.map +1 -1
- package/dist/tools/append-session-entry.d.ts +2 -0
- package/dist/tools/append-session-entry.d.ts.map +1 -1
- package/dist/tools/append-session-entry.js +2 -0
- package/dist/tools/append-session-entry.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/machine-id.test.ts +136 -0
- package/src/__tests__/nexus-api.test.ts +127 -0
- package/src/machine-id.ts +60 -0
- package/src/nexus-api.ts +14 -0
- package/src/tools/append-session-entry.ts +2 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine ID module for Nexus MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Reads or generates a persistent machine identifier stored at
|
|
5
|
+
* ~/.config/nexus/machine.toml. This ID is sent as X-Nexus-Machine-Id
|
|
6
|
+
* header with every API request so session entries can be attributed
|
|
7
|
+
* to a specific machine.
|
|
8
|
+
*
|
|
9
|
+
* Format of machine.toml:
|
|
10
|
+
* machine_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Get the persistent machine ID, creating one if it doesn't exist.
|
|
14
|
+
* Returns null only if file I/O fails (e.g. read-only filesystem).
|
|
15
|
+
*/
|
|
16
|
+
export declare function getMachineId(): string | null;
|
|
17
|
+
/** Reset cached machine ID (for testing). */
|
|
18
|
+
export declare function resetMachineId(): void;
|
|
19
|
+
//# sourceMappingURL=machine-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-id.d.ts","sourceRoot":"","sources":["../src/machine-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAYH;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CA4B5C;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,IAAI,IAAI,CAErC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine ID module for Nexus MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Reads or generates a persistent machine identifier stored at
|
|
5
|
+
* ~/.config/nexus/machine.toml. This ID is sent as X-Nexus-Machine-Id
|
|
6
|
+
* header with every API request so session entries can be attributed
|
|
7
|
+
* to a specific machine.
|
|
8
|
+
*
|
|
9
|
+
* Format of machine.toml:
|
|
10
|
+
* machine_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
const CONFIG_DIR = join(homedir(), '.config', 'nexus');
|
|
17
|
+
const MACHINE_TOML = join(CONFIG_DIR, 'machine.toml');
|
|
18
|
+
let _cachedId = null;
|
|
19
|
+
/**
|
|
20
|
+
* Get the persistent machine ID, creating one if it doesn't exist.
|
|
21
|
+
* Returns null only if file I/O fails (e.g. read-only filesystem).
|
|
22
|
+
*/
|
|
23
|
+
export function getMachineId() {
|
|
24
|
+
if (_cachedId)
|
|
25
|
+
return _cachedId;
|
|
26
|
+
try {
|
|
27
|
+
// Try to read existing
|
|
28
|
+
if (existsSync(MACHINE_TOML)) {
|
|
29
|
+
const content = readFileSync(MACHINE_TOML, 'utf-8');
|
|
30
|
+
const match = content.match(/machine_id\s*=\s*"([^"]+)"/);
|
|
31
|
+
if (match?.[1]) {
|
|
32
|
+
_cachedId = match[1];
|
|
33
|
+
return _cachedId;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Generate new machine ID
|
|
37
|
+
const id = randomUUID();
|
|
38
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
writeFileSync(MACHINE_TOML, `# Nexus persistent machine identifier\n# Generated automatically — do not edit\nmachine_id = "${id}"\n`, 'utf-8');
|
|
40
|
+
_cachedId = id;
|
|
41
|
+
return _cachedId;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Silently fail — machine_id is optional enrichment
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Reset cached machine ID (for testing). */
|
|
49
|
+
export function resetMachineId() {
|
|
50
|
+
_cachedId = null;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=machine-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-id.js","sourceRoot":"","sources":["../src/machine-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;AACtD,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAA;AAErD,IAAI,SAAS,GAAkB,IAAI,CAAA;AAEnC;;;GAGG;AACH,MAAM,UAAU,YAAY;IAC1B,IAAI,SAAS;QAAE,OAAO,SAAS,CAAA;IAE/B,IAAI,CAAC;QACH,uBAAuB;QACvB,IAAI,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;YACnD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAA;YACzD,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACf,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;gBACpB,OAAO,SAAS,CAAA;YAClB,CAAC;QACH,CAAC;QAED,0BAA0B;QAC1B,MAAM,EAAE,GAAG,UAAU,EAAE,CAAA;QACvB,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC1C,aAAa,CACX,YAAY,EACZ,iGAAiG,EAAE,KAAK,EACxG,OAAO,CACR,CAAA;QACD,SAAS,GAAG,EAAE,CAAA;QACd,OAAO,SAAS,CAAA;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,oDAAoD;QACpD,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,cAAc;IAC5B,SAAS,GAAG,IAAI,CAAA;AAClB,CAAC"}
|
package/dist/nexus-api.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface NexusApiResponse<T = unknown> {
|
|
|
22
22
|
export declare function nexusApi<T = unknown>(path: string, options?: {
|
|
23
23
|
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
24
24
|
body?: unknown;
|
|
25
|
+
headers?: Record<string, string>;
|
|
25
26
|
timeoutMs?: number;
|
|
26
27
|
}): Promise<NexusApiResponse<T>>;
|
|
27
28
|
/**
|
package/dist/nexus-api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nexus-api.d.ts","sourceRoot":"","sources":["../src/nexus-api.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"nexus-api.d.ts","sourceRoot":"","sources":["../src/nexus-api.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH;;;GAGG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAIrC;AA0BD,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,OAAO;IAC3C,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,CAAC,GAAG,IAAI,CAAA;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAKD;;GAEG;AACH,wBAAsB,QAAQ,CAAC,CAAC,GAAG,OAAO,EACxC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;IAC5C,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;CACd,GACL,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CA6C9B;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,CAAC,GAAG,OAAO,EACzC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAE9B;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,CAAC,GAAG,OAAO,EACxC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAE9B"}
|
package/dist/nexus-api.js
CHANGED
|
@@ -5,8 +5,17 @@
|
|
|
5
5
|
* All operations go through /api/mcp/* endpoints, authenticated
|
|
6
6
|
* via the NEXUS_PRIVATE_TOKEN (nxs_pat_*) Bearer token.
|
|
7
7
|
*/
|
|
8
|
+
import { getMachineId } from './machine-id.js';
|
|
8
9
|
let _baseUrl = null;
|
|
9
10
|
let _token = null;
|
|
11
|
+
let _machineId = null;
|
|
12
|
+
// Eagerly resolve machine ID (sync, cached after first call)
|
|
13
|
+
try {
|
|
14
|
+
_machineId = getMachineId();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// machine_id is optional enrichment — never block startup
|
|
18
|
+
}
|
|
10
19
|
/**
|
|
11
20
|
* Reset the cached API configuration. Used by E2E tests to switch
|
|
12
21
|
* from mocked env vars to real credentials.
|
|
@@ -14,6 +23,7 @@ let _token = null;
|
|
|
14
23
|
export function resetApiConfig() {
|
|
15
24
|
_baseUrl = null;
|
|
16
25
|
_token = null;
|
|
26
|
+
_machineId = getMachineId();
|
|
17
27
|
}
|
|
18
28
|
function getConfig() {
|
|
19
29
|
if (_baseUrl && _token)
|
|
@@ -43,6 +53,8 @@ export async function nexusApi(path, options = {}) {
|
|
|
43
53
|
const headers = {
|
|
44
54
|
Authorization: `Bearer ${token}`,
|
|
45
55
|
'Content-Type': 'application/json',
|
|
56
|
+
...(_machineId ? { 'X-Nexus-Machine-Id': _machineId } : {}),
|
|
57
|
+
...(options.headers ?? {}),
|
|
46
58
|
};
|
|
47
59
|
try {
|
|
48
60
|
const fetchOpts = {
|
package/dist/nexus-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nexus-api.js","sourceRoot":"","sources":["../src/nexus-api.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,IAAI,QAAQ,GAAkB,IAAI,CAAA;AAClC,IAAI,MAAM,GAAkB,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"nexus-api.js","sourceRoot":"","sources":["../src/nexus-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,6 +21,8 @@ export declare const appendSessionEntrySchema: {
|
|
|
21
21
|
handoff_recorded: "handoff_recorded";
|
|
22
22
|
note: "note";
|
|
23
23
|
correction: "correction";
|
|
24
|
+
security_scan: "security_scan";
|
|
25
|
+
performance_scan: "performance_scan";
|
|
24
26
|
}>;
|
|
25
27
|
summary: z.ZodString;
|
|
26
28
|
linked_entity_type: z.ZodOptional<z.ZodString>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"append-session-entry.d.ts","sourceRoot":"","sources":["../../src/tools/append-session-entry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,eAAO,MAAM,wBAAwB
|
|
1
|
+
{"version":3,"file":"append-session-entry.d.ts","sourceRoot":"","sources":["../../src/tools/append-session-entry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;CA0CpC,CAAA;AAED,KAAK,sBAAsB,GAAG;IAC5B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB;;;;;;;;;;;;GA8CpE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"append-session-entry.js","sourceRoot":"","sources":["../../src/tools/append-session-entry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,2BAA2B,CAAC;IACnE,UAAU,EAAE,CAAC;SACV,IAAI,CAAC;QACJ,qBAAqB;QACrB,cAAc;QACd,cAAc;QACd,aAAa;QACb,gBAAgB;QAChB,gBAAgB;QAChB,mBAAmB;QACnB,aAAa;QACb,cAAc;QACd,kBAAkB;QAClB,MAAM;QACN,YAAY;
|
|
1
|
+
{"version":3,"file":"append-session-entry.js","sourceRoot":"","sources":["../../src/tools/append-session-entry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for src/machine-id.ts
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Reading existing machine.toml
|
|
6
|
+
* - Generating new machine ID when file doesn't exist
|
|
7
|
+
* - Caching behavior
|
|
8
|
+
* - Graceful failure on I/O errors
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
|
|
16
|
+
// Mock fs and os modules
|
|
17
|
+
vi.mock('node:fs', () => ({
|
|
18
|
+
existsSync: vi.fn(),
|
|
19
|
+
mkdirSync: vi.fn(),
|
|
20
|
+
readFileSync: vi.fn(),
|
|
21
|
+
writeFileSync: vi.fn(),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
vi.mock('node:os', () => ({
|
|
25
|
+
homedir: vi.fn(() => '/mock-home'),
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
const EXPECTED_TOML_PATH = join('/mock-home', '.config', 'nexus', 'machine.toml')
|
|
29
|
+
|
|
30
|
+
describe('machine-id', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.resetModules()
|
|
33
|
+
vi.restoreAllMocks()
|
|
34
|
+
vi.mocked(homedir).mockReturnValue('/mock-home')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.restoreAllMocks()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should read machine_id from existing machine.toml', async () => {
|
|
42
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
43
|
+
vi.mocked(readFileSync).mockReturnValue(
|
|
44
|
+
'# Nexus persistent machine identifier\nmachine_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"\n',
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const { getMachineId, resetMachineId } = await import('../machine-id.js')
|
|
48
|
+
resetMachineId()
|
|
49
|
+
const id = getMachineId()
|
|
50
|
+
|
|
51
|
+
expect(id).toBe('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
|
52
|
+
expect(existsSync).toHaveBeenCalledWith(EXPECTED_TOML_PATH)
|
|
53
|
+
expect(readFileSync).toHaveBeenCalledWith(EXPECTED_TOML_PATH, 'utf-8')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should generate and persist machine_id when file does not exist', async () => {
|
|
57
|
+
vi.mocked(existsSync).mockReturnValue(false)
|
|
58
|
+
|
|
59
|
+
const { getMachineId, resetMachineId } = await import('../machine-id.js')
|
|
60
|
+
resetMachineId()
|
|
61
|
+
const id = getMachineId()
|
|
62
|
+
|
|
63
|
+
expect(id).toBeDefined()
|
|
64
|
+
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
|
|
65
|
+
expect(mkdirSync).toHaveBeenCalledWith(
|
|
66
|
+
join('/mock-home', '.config', 'nexus'),
|
|
67
|
+
{ recursive: true },
|
|
68
|
+
)
|
|
69
|
+
expect(writeFileSync).toHaveBeenCalledWith(
|
|
70
|
+
EXPECTED_TOML_PATH,
|
|
71
|
+
expect.stringContaining(`machine_id = "${id}"`),
|
|
72
|
+
'utf-8',
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should cache the machine_id after first read', async () => {
|
|
77
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
78
|
+
vi.mocked(readFileSync).mockReturnValue('machine_id = "cached-id-1234"\n')
|
|
79
|
+
|
|
80
|
+
const { getMachineId, resetMachineId } = await import('../machine-id.js')
|
|
81
|
+
resetMachineId()
|
|
82
|
+
|
|
83
|
+
const callsBefore = vi.mocked(readFileSync).mock.calls.length
|
|
84
|
+
const id1 = getMachineId()
|
|
85
|
+
const id2 = getMachineId()
|
|
86
|
+
|
|
87
|
+
expect(id1).toBe('cached-id-1234')
|
|
88
|
+
expect(id1).toBe(id2)
|
|
89
|
+
// readFileSync should only be called once more after reset (cached on second call)
|
|
90
|
+
expect(vi.mocked(readFileSync).mock.calls.length - callsBefore).toBe(1)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should return null on I/O error', async () => {
|
|
94
|
+
vi.mocked(existsSync).mockImplementation(() => {
|
|
95
|
+
throw new Error('Permission denied')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const { getMachineId, resetMachineId } = await import('../machine-id.js')
|
|
99
|
+
resetMachineId()
|
|
100
|
+
const id = getMachineId()
|
|
101
|
+
|
|
102
|
+
expect(id).toBeNull()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should generate new ID if toml exists but has no machine_id line', async () => {
|
|
106
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
107
|
+
vi.mocked(readFileSync).mockReturnValue('# empty config\n')
|
|
108
|
+
|
|
109
|
+
const { getMachineId, resetMachineId } = await import('../machine-id.js')
|
|
110
|
+
resetMachineId()
|
|
111
|
+
const id = getMachineId()
|
|
112
|
+
|
|
113
|
+
expect(id).toBeDefined()
|
|
114
|
+
expect(id).toMatch(/^[0-9a-f]{8}-/)
|
|
115
|
+
expect(writeFileSync).toHaveBeenCalled()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should resetMachineId clear the cache', async () => {
|
|
119
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
120
|
+
vi.mocked(readFileSync)
|
|
121
|
+
.mockReturnValueOnce('machine_id = "first-id"\n')
|
|
122
|
+
.mockReturnValueOnce('machine_id = "second-id"\n')
|
|
123
|
+
|
|
124
|
+
const { getMachineId, resetMachineId } = await import('../machine-id.js')
|
|
125
|
+
resetMachineId()
|
|
126
|
+
|
|
127
|
+
const id1 = getMachineId()
|
|
128
|
+
expect(id1).toBe('first-id')
|
|
129
|
+
|
|
130
|
+
resetMachineId()
|
|
131
|
+
const id2 = getMachineId()
|
|
132
|
+
expect(id2).toBe('second-id')
|
|
133
|
+
|
|
134
|
+
expect(id1).not.toBe(id2)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for src/nexus-api.ts
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - X-Nexus-Machine-Id header is sent automatically when machine_id is available
|
|
6
|
+
* - Custom headers parameter is forwarded
|
|
7
|
+
* - nexusPost convenience works correctly
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
11
|
+
|
|
12
|
+
// Mock machine-id module before importing nexus-api
|
|
13
|
+
vi.mock('../machine-id.js', () => ({
|
|
14
|
+
getMachineId: vi.fn(() => 'test-machine-uuid-1234'),
|
|
15
|
+
resetMachineId: vi.fn(),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
describe('nexus-api: header injection', () => {
|
|
19
|
+
let nexusApi: typeof import('../nexus-api.js').nexusApi
|
|
20
|
+
let nexusPost: typeof import('../nexus-api.js').nexusPost
|
|
21
|
+
let fetchSpy: ReturnType<typeof vi.fn>
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
vi.resetModules()
|
|
25
|
+
|
|
26
|
+
process.env.NEXUS_API_URL = 'https://test.nexus.example.com'
|
|
27
|
+
process.env.NEXUS_PRIVATE_TOKEN = 'nxs_pat_test-token-for-header-tests-abc'
|
|
28
|
+
|
|
29
|
+
fetchSpy = vi.fn().mockResolvedValue({
|
|
30
|
+
ok: true,
|
|
31
|
+
status: 200,
|
|
32
|
+
json: async () => ({ action: 'test' }),
|
|
33
|
+
})
|
|
34
|
+
vi.stubGlobal('fetch', fetchSpy)
|
|
35
|
+
|
|
36
|
+
const mod = await import('../nexus-api.js')
|
|
37
|
+
nexusApi = mod.nexusApi
|
|
38
|
+
nexusPost = mod.nexusPost
|
|
39
|
+
mod.resetApiConfig()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.unstubAllGlobals()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should include X-Nexus-Machine-Id header in requests', async () => {
|
|
47
|
+
await nexusApi('/api/mcp/sessions', { method: 'POST', body: { action: 'test' } })
|
|
48
|
+
|
|
49
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
50
|
+
const [, opts] = fetchSpy.mock.calls[0]
|
|
51
|
+
expect(opts.headers['X-Nexus-Machine-Id']).toBe('test-machine-uuid-1234')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should include Authorization and Content-Type headers', async () => {
|
|
55
|
+
await nexusApi('/api/mcp/sessions')
|
|
56
|
+
|
|
57
|
+
const [, opts] = fetchSpy.mock.calls[0]
|
|
58
|
+
expect(opts.headers['Authorization']).toBe(
|
|
59
|
+
'Bearer nxs_pat_test-token-for-header-tests-abc',
|
|
60
|
+
)
|
|
61
|
+
expect(opts.headers['Content-Type']).toBe('application/json')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should merge custom headers with defaults', async () => {
|
|
65
|
+
await nexusApi('/api/mcp/sessions', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
body: { action: 'test' },
|
|
68
|
+
headers: { 'X-Nexus-Toolstack': 'opencode v0.3.0' },
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const [, opts] = fetchSpy.mock.calls[0]
|
|
72
|
+
expect(opts.headers['X-Nexus-Machine-Id']).toBe('test-machine-uuid-1234')
|
|
73
|
+
expect(opts.headers['X-Nexus-Toolstack']).toBe('opencode v0.3.0')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should allow custom headers to override machine-id', async () => {
|
|
77
|
+
await nexusApi('/api/mcp/sessions', {
|
|
78
|
+
headers: { 'X-Nexus-Machine-Id': 'override-id' },
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const [, opts] = fetchSpy.mock.calls[0]
|
|
82
|
+
expect(opts.headers['X-Nexus-Machine-Id']).toBe('override-id')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('nexusPost should also include machine-id header', async () => {
|
|
86
|
+
await nexusPost('/api/mcp/sessions', { action: 'session_create' })
|
|
87
|
+
|
|
88
|
+
const [, opts] = fetchSpy.mock.calls[0]
|
|
89
|
+
expect(opts.headers['X-Nexus-Machine-Id']).toBe('test-machine-uuid-1234')
|
|
90
|
+
expect(opts.method).toBe('POST')
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('nexus-api: no machine-id available', () => {
|
|
95
|
+
beforeEach(async () => {
|
|
96
|
+
vi.resetModules()
|
|
97
|
+
// Re-mock with null return
|
|
98
|
+
vi.doMock('../machine-id.js', () => ({
|
|
99
|
+
getMachineId: vi.fn(() => null),
|
|
100
|
+
resetMachineId: vi.fn(),
|
|
101
|
+
}))
|
|
102
|
+
|
|
103
|
+
process.env.NEXUS_API_URL = 'https://test.nexus.example.com'
|
|
104
|
+
process.env.NEXUS_PRIVATE_TOKEN = 'nxs_pat_test-token-no-machine-id-abcde'
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
vi.unstubAllGlobals()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should not include X-Nexus-Machine-Id header when machine_id is null', async () => {
|
|
112
|
+
const fetchSpy = vi.fn().mockResolvedValue({
|
|
113
|
+
ok: true,
|
|
114
|
+
status: 200,
|
|
115
|
+
json: async () => ({ action: 'test' }),
|
|
116
|
+
})
|
|
117
|
+
vi.stubGlobal('fetch', fetchSpy)
|
|
118
|
+
|
|
119
|
+
const mod = await import('../nexus-api.js')
|
|
120
|
+
mod.resetApiConfig()
|
|
121
|
+
|
|
122
|
+
await mod.nexusApi('/api/mcp/sessions')
|
|
123
|
+
|
|
124
|
+
const [, opts] = fetchSpy.mock.calls[0]
|
|
125
|
+
expect(opts.headers).not.toHaveProperty('X-Nexus-Machine-Id')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine ID module for Nexus MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Reads or generates a persistent machine identifier stored at
|
|
5
|
+
* ~/.config/nexus/machine.toml. This ID is sent as X-Nexus-Machine-Id
|
|
6
|
+
* header with every API request so session entries can be attributed
|
|
7
|
+
* to a specific machine.
|
|
8
|
+
*
|
|
9
|
+
* Format of machine.toml:
|
|
10
|
+
* machine_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { randomUUID } from 'node:crypto'
|
|
17
|
+
|
|
18
|
+
const CONFIG_DIR = join(homedir(), '.config', 'nexus')
|
|
19
|
+
const MACHINE_TOML = join(CONFIG_DIR, 'machine.toml')
|
|
20
|
+
|
|
21
|
+
let _cachedId: string | null = null
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the persistent machine ID, creating one if it doesn't exist.
|
|
25
|
+
* Returns null only if file I/O fails (e.g. read-only filesystem).
|
|
26
|
+
*/
|
|
27
|
+
export function getMachineId(): string | null {
|
|
28
|
+
if (_cachedId) return _cachedId
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Try to read existing
|
|
32
|
+
if (existsSync(MACHINE_TOML)) {
|
|
33
|
+
const content = readFileSync(MACHINE_TOML, 'utf-8')
|
|
34
|
+
const match = content.match(/machine_id\s*=\s*"([^"]+)"/)
|
|
35
|
+
if (match?.[1]) {
|
|
36
|
+
_cachedId = match[1]
|
|
37
|
+
return _cachedId
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Generate new machine ID
|
|
42
|
+
const id = randomUUID()
|
|
43
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
44
|
+
writeFileSync(
|
|
45
|
+
MACHINE_TOML,
|
|
46
|
+
`# Nexus persistent machine identifier\n# Generated automatically — do not edit\nmachine_id = "${id}"\n`,
|
|
47
|
+
'utf-8',
|
|
48
|
+
)
|
|
49
|
+
_cachedId = id
|
|
50
|
+
return _cachedId
|
|
51
|
+
} catch {
|
|
52
|
+
// Silently fail — machine_id is optional enrichment
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Reset cached machine ID (for testing). */
|
|
58
|
+
export function resetMachineId(): void {
|
|
59
|
+
_cachedId = null
|
|
60
|
+
}
|
package/src/nexus-api.ts
CHANGED
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
* via the NEXUS_PRIVATE_TOKEN (nxs_pat_*) Bearer token.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { getMachineId } from './machine-id.js'
|
|
10
|
+
|
|
9
11
|
let _baseUrl: string | null = null
|
|
10
12
|
let _token: string | null = null
|
|
13
|
+
let _machineId: string | null = null
|
|
14
|
+
|
|
15
|
+
// Eagerly resolve machine ID (sync, cached after first call)
|
|
16
|
+
try {
|
|
17
|
+
_machineId = getMachineId()
|
|
18
|
+
} catch {
|
|
19
|
+
// machine_id is optional enrichment — never block startup
|
|
20
|
+
}
|
|
11
21
|
|
|
12
22
|
/**
|
|
13
23
|
* Reset the cached API configuration. Used by E2E tests to switch
|
|
@@ -16,6 +26,7 @@ let _token: string | null = null
|
|
|
16
26
|
export function resetApiConfig(): void {
|
|
17
27
|
_baseUrl = null
|
|
18
28
|
_token = null
|
|
29
|
+
_machineId = getMachineId()
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
function getConfig(): { baseUrl: string; token: string } {
|
|
@@ -60,6 +71,7 @@ export async function nexusApi<T = unknown>(
|
|
|
60
71
|
options: {
|
|
61
72
|
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'
|
|
62
73
|
body?: unknown
|
|
74
|
+
headers?: Record<string, string>
|
|
63
75
|
timeoutMs?: number
|
|
64
76
|
} = {},
|
|
65
77
|
): Promise<NexusApiResponse<T>> {
|
|
@@ -70,6 +82,8 @@ export async function nexusApi<T = unknown>(
|
|
|
70
82
|
const headers: Record<string, string> = {
|
|
71
83
|
Authorization: `Bearer ${token}`,
|
|
72
84
|
'Content-Type': 'application/json',
|
|
85
|
+
...(_machineId ? { 'X-Nexus-Machine-Id': _machineId } : {}),
|
|
86
|
+
...(options.headers ?? {}),
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
try {
|