trackqa 0.1.1 → 0.1.2

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 CHANGED
@@ -7,19 +7,21 @@ credentials. The connector does not install or authenticate the AI client itself
7
7
 
8
8
  ## Package status and installation
9
9
 
10
- **trackqa@0.1.0** was published and independently verified on November 4, 2026.
11
- The locally prepared **v0.1.1 candidate** removes manual public-key setup for the
12
- official hosted TrackQA service. **v0.1.1 is not published; publication requires
13
- separate approval.** The original v0.1.0 artifact is retained unchanged.
10
+ **trackqa@0.1.1** was published and independently verified on November 5, 2026.
11
+ The local **v0.1.2 candidate** fixes Codex launches from the Windows user home:
12
+ the standard profile and application-data directories are no longer incorrectly
13
+ rejected as workspace-local configuration. Workspace and package boundaries
14
+ remain enforced. **v0.1.2 is not published; publication requires separate approval.**
15
+ The published v0.1.0 and v0.1.1 artifacts are retained unchanged.
14
16
 
15
17
  To verify an approved local tarball in a disposable directory:
16
18
 
17
19
  ```powershell
18
- npm install --ignore-scripts --no-audit --no-fund <absolute-path-to-trackqa-0.1.1.tgz>
20
+ npm install --ignore-scripts --no-audit --no-fund <absolute-path-to-trackqa-0.1.2.tgz>
19
21
  npm exec --offline -- trackqa --help
20
22
  ```
21
23
 
22
- After v0.1.1 is approved and published, the official hosted workflow is:
24
+ The official hosted workflow is:
23
25
 
24
26
  ```powershell
25
27
  npx trackqa connect
@@ -29,9 +31,10 @@ npx trackqa status --client codex
29
31
  npx trackqa disconnect --client codex --project <project-UUID>
30
32
  ```
31
33
 
32
- Until v0.1.1 is published, the bare registry command still resolves to v0.1.0 and
33
- retains that version's manual public-key requirement. Validate this candidate
34
- through a local tarball installation and `npm exec --offline -- trackqa ...`.
34
+ Until v0.1.2 is published, the bare registry command still resolves to v0.1.1.
35
+ With v0.1.1, launch from your coding project folder rather than the user home to
36
+ avoid `configuration_location_untrusted`. Validate the v0.1.2 candidate through
37
+ a local tarball installation and `npm exec --offline -- trackqa ...`.
35
38
  Bare `connect` prompts for a client; `--client codex` is optional. No client is
36
39
  selected by default.
37
40
 
@@ -7,12 +7,7 @@ export const mcpTools = [
7
7
  { name: 'trackqa_get_issue', description: 'Read an issue in that project.' },
8
8
  { name: 'trackqa_submit_finding', description: 'Submit a potential bug for human QA/Owner review.' },
9
9
  ];
10
- export const clientDocs = {
11
- codex: 'https://developers.openai.com/codex/mcp/',
12
- claude: 'https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcpjson',
13
- };
14
- // These builders never accept a token. UUID-derived identifiers avoid project
15
- // name/key injection and keep credentials for different projects independent.
10
+ // UUID-derived identifiers keep project configurations independent.
16
11
  export function mcpSetup(supabaseUrl, projectId) {
17
12
  let url;
18
13
  try {
@@ -29,61 +24,6 @@ export function mcpSetup(supabaseUrl, projectId) {
29
24
  return { endpoint: `${url.origin}/functions/v1/trackqa-mcp`, serverName: `trackqa_${id}`,
30
25
  envVar: `TRACKQA_MCP_${id.toUpperCase()}_TOKEN`, projectId: projectId.toLowerCase() };
31
26
  }
32
- function verifySetup(setup) {
33
- try {
34
- const expected = mcpSetup(new URL(setup.endpoint).origin, setup.projectId);
35
- if (setup.endpoint === expected.endpoint && setup.serverName === expected.serverName && setup.envVar === expected.envVar)
36
- return;
37
- }
38
- catch { /* Return only a fixed configuration error. */ }
39
- throw new Error('Client setup is unavailable.');
40
- }
41
- export function clientConfig(setup, client) {
42
- verifySetup(setup);
43
- if (!['codex', 'claude', 'generic'].includes(client))
44
- throw new Error('Client setup is unavailable.');
45
- if (client === 'generic')
46
- return null; // There is no universal client config schema.
47
- if (client === 'claude')
48
- return JSON.stringify({ mcpServers: { [setup.serverName]: {
49
- type: 'http', url: setup.endpoint, headers: { Authorization: 'Bearer ${' + setup.envVar + '}' },
50
- } } }, null, 2);
51
- return `[mcp_servers.${setup.serverName}]\nurl = ${JSON.stringify(setup.endpoint)}\nbearer_token_env_var = ${JSON.stringify(setup.envVar)}\nenabled_tools = [${mcpTools.map(tool => JSON.stringify(tool.name)).join(', ')}]`;
52
- }
53
- export function clientLaunch(setup, client, terminal) {
54
- verifySetup(setup);
55
- // Only fixed executable names and validated variable identifiers enter code.
56
- if (!/^TRACKQA_MCP_[0-9A-F]{32}_TOKEN$/.test(setup.envVar)
57
- || !['codex', 'claude'].includes(client) || !['powershell', 'bash'].includes(terminal)) {
58
- throw new Error('Client setup is unavailable.');
59
- }
60
- const variable = setup.envVar;
61
- if (terminal === 'bash')
62
- return `(
63
- set +x
64
- trap 'unset ${variable}' EXIT
65
- if ! read -r -s -p 'TrackQA token (hidden): ' ${variable}; then exit 1; fi
66
- printf '\\n'
67
- if [[ ! "$${variable}" =~ ^tqm_[0-9a-f]{64}$ ]]; then printf 'Invalid TrackQA token format.\\n' >&2; exit 1; fi
68
- export ${variable}
69
- ${client}
70
- )`;
71
- return `& {
72
- $ErrorActionPreference = 'Stop'
73
- $secureToken = Read-Host 'TrackQA token (hidden)' -AsSecureString
74
- $tokenPointer = [IntPtr]::Zero
75
- try {
76
- $tokenPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken)
77
- $env:${variable} = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($tokenPointer)
78
- if ($env:${variable} -notmatch '^tqm_[0-9a-f]{64}$') { throw 'Invalid TrackQA token format.' }
79
- ${client}
80
- } finally {
81
- Remove-Item Env:${variable} -ErrorAction SilentlyContinue
82
- if ($tokenPointer -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($tokenPointer) }
83
- $secureToken.Dispose()
84
- }
85
- }`;
86
- }
87
27
  export function connectionStatus(connection, enabled, archived, memberPresent, now) {
88
28
  if (connection.revoked_at)
89
29
  return 'Revoked';
@@ -98,46 +38,3 @@ export function connectionStatus(connection, enabled, archived, memberPresent, n
98
38
  return 'Project disabled';
99
39
  return 'Ready to connect';
100
40
  }
101
- export function watchTokenReveal(clear, documentTarget, windowTarget) {
102
- const hide = () => { if (documentTarget.hidden)
103
- clear(); };
104
- const timer = windowTarget.setTimeout(clear, 120_000);
105
- documentTarget.addEventListener('visibilitychange', hide);
106
- windowTarget.addEventListener('pagehide', clear);
107
- return () => {
108
- windowTarget.clearTimeout(timer);
109
- documentTarget.removeEventListener('visibilitychange', hide);
110
- windowTarget.removeEventListener('pagehide', clear);
111
- };
112
- }
113
- const issuanceError = () => new Error('Token issuance could not be completed. Refresh connections and revoke any new entry before trying again.');
114
- // The existing human-authenticated RPC remains authoritative. No retry, cache,
115
- // persistence, diagnostics, or token-bearing return value is introduced here.
116
- export async function issueConnectionToken(call, scope, label, reveal) {
117
- let rows = null;
118
- try {
119
- if (!scope.isOwner || !scope.enabled || scope.archived || !uuid.test(scope.projectId)
120
- || !uuid.test(scope.userId) || !scope.memberIds.includes(scope.userId)
121
- || !label.trim() || label.trim().length > 120)
122
- throw issuanceError();
123
- const result = await call({ p_project_id: scope.projectId, p_user_id: scope.userId, p_client_label: label.trim() });
124
- rows = result.data;
125
- if (result.error || !Array.isArray(rows) || rows.length !== 1 || !uuid.test(rows[0]?.connection_id)
126
- || !/^tqm_[0-9a-f]{64}$/.test(rows[0]?.access_token))
127
- throw issuanceError();
128
- const connectionId = rows[0].connection_id;
129
- const displayed = reveal({ connectionId, token: rows[0].access_token });
130
- return { connectionId, displayed };
131
- }
132
- catch {
133
- throw issuanceError();
134
- }
135
- finally {
136
- // Discard the SDK response's one-time secret after handing it to the local
137
- // reveal dialog. It must never enter connection metadata or a query cache.
138
- if (Array.isArray(rows))
139
- for (const row of rows)
140
- if (row && typeof row === 'object')
141
- row.access_token = '';
142
- }
143
- }
@@ -16,7 +16,7 @@ import { adapterLabel } from "./clients/adapter.js";
16
16
  import { ConnectorError, display, parseArgs, requireValue } from "./core.js";
17
17
  import { Store } from "./state.js";
18
18
  import { connect, disconnect, status } from "./workflow.js";
19
- export const help = `TrackQA Connect 0.1.1 - Windows, Owner-operated MVP release candidate
19
+ export const help = `TrackQA Connect 0.1.2 - Windows, Owner-operated MVP release candidate
20
20
  Usage after installing the approved package or local tarball:
21
21
  trackqa connect --client codex
22
22
  trackqa status --client codex
@@ -39,17 +39,33 @@ Advanced: TRACKQA_PUBLIC_KEY or VITE_SUPABASE_ANON_KEY overrides the public key
39
39
  No bearer token is saved. The existing 90-day token expiry is unchanged; session cleanup attempts and independently verifies revocation.
40
40
  Status is local-only: it does not authorize or test a live MCP connection.
41
41
  Disconnect may require fresh Google Owner sign-in. Never pass credentials as arguments.`;
42
+ const samePath = (a, b) => path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
43
+ const under = (target, base) => {
44
+ const relative = path.relative(path.resolve(base).toLowerCase(), path.resolve(target).toLowerCase());
45
+ return relative === '' || (relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative));
46
+ };
47
+ export function isSafeConfigurationRoot(target, cwd, repo, home, defaultRoot) {
48
+ if (![target, cwd, repo, home, defaultRoot].every(value => path.isAbsolute(value)))
49
+ return false;
50
+ // Home is also a common shell launch directory. Exempt only the exact
51
+ // default root there, never a workspace-local override or the package tree.
52
+ return !under(target, repo) && (!under(target, cwd) || (samePath(cwd, home) && samePath(target, defaultRoot)));
53
+ }
42
54
  export async function locations(env = process.env, client = 'codex') {
43
55
  const codexHome = env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
44
56
  if (client === 'codex')
45
57
  requireValue(path.isAbsolute(codexHome), 'configuration_location_untrusted');
46
58
  const canonical = await realpath(codexHome).catch(() => path.resolve(codexHome));
47
59
  const repo = fileURLToPath(new URL('../../../', import.meta.url));
48
- const under = (target, base) => target.toLowerCase() === base.toLowerCase() || target.toLowerCase().startsWith(path.resolve(base).toLowerCase() + path.sep);
60
+ const cwd = await realpath(process.cwd());
61
+ const home = await realpath(os.homedir()).catch(() => path.resolve(os.homedir()));
49
62
  if (client === 'codex')
50
- requireValue(!under(canonical, process.cwd()) && !under(canonical, repo), 'configuration_location_untrusted');
63
+ requireValue(isSafeConfigurationRoot(canonical, cwd, repo, home, path.join(home, '.codex')), 'configuration_location_untrusted');
51
64
  const data = process.platform === 'win32' ? env.LOCALAPPDATA : env.XDG_STATE_HOME ?? path.join(os.homedir(), '.local', 'state');
52
- requireValue(data && path.isAbsolute(data) && !under(data, repo) && !under(data, process.cwd()), 'recovery_unavailable');
65
+ requireValue(data && path.isAbsolute(data), 'recovery_unavailable');
66
+ const canonicalData = await realpath(data).catch(() => path.resolve(data));
67
+ const defaultData = process.platform === 'win32' ? path.join(home, 'AppData', 'Local') : path.join(home, '.local', 'state');
68
+ requireValue(isSafeConfigurationRoot(canonicalData, cwd, repo, home, defaultData), 'recovery_unavailable');
53
69
  const geminiHome = env.GEMINI_CLI_HOME ?? os.homedir();
54
70
  const geminiCanonical = await realpath(geminiHome).catch(() => path.resolve(geminiHome));
55
71
  if (client === 'gemini') {
@@ -63,7 +63,7 @@ export async function verifyCodex(executable, codexHome, setup, token, signal, s
63
63
  child.stdin.write(JSON.stringify({ id, method, params }) + '\n');
64
64
  });
65
65
  try {
66
- await rpc('initialize', { clientInfo: { name: 'trackqa_connect', version: '0.1.1' } });
66
+ await rpc('initialize', { clientInfo: { name: 'trackqa_connect', version: '0.1.2' } });
67
67
  child.stdin.write(JSON.stringify({ method: 'initialized', params: {} }) + '\n');
68
68
  const config = await rpc('config/read');
69
69
  const servers = config?.config?.mcp_servers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackqa",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Owner-operated TrackQA MCP connector for Windows",
5
5
  "type": "module",
6
6
  "bin": { "trackqa": "dist/tools/trackqa-connect/src/cli.js" },