trackqa 0.1.0 → 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,22 +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
- The locally prepared release candidate is **trackqa@0.1.0**, with the `trackqa`
11
- executable. On November 4, 2026, npm identity `eronny`, verified email and completed
12
- `auth-and-writes` 2FA were checked. The registry returned 404 for `trackqa`; the name
13
- has not been reserved and first-publication acceptance is not guaranteed. The
14
- private flag was removed only for authorized local release-candidate preparation.
15
- **The package has not been published. Publication requires separate approval.**
16
- No alternative release identity is substituted.
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.
17
16
 
18
17
  To verify an approved local tarball in a disposable directory:
19
18
 
20
19
  ```powershell
21
- npm install --ignore-scripts --no-audit --no-fund <absolute-path-to-trackqa-0.1.0.tgz>
20
+ npm install --ignore-scripts --no-audit --no-fund <absolute-path-to-trackqa-0.1.2.tgz>
22
21
  npm exec --offline -- trackqa --help
23
22
  ```
24
23
 
25
- After the intended package is approved and published, the target workflow is:
24
+ The official hosted workflow is:
26
25
 
27
26
  ```powershell
28
27
  npx trackqa connect
@@ -32,9 +31,12 @@ npx trackqa status --client codex
32
31
  npx trackqa disconnect --client codex --project <project-UUID>
33
32
  ```
34
33
 
35
- These registry commands are a future distribution workflow, not instructions to
36
- fetch an unverified package now. With a local installation use
37
- `npm exec --offline -- trackqa ...`. No client is selected by default.
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 ...`.
38
+ Bare `connect` prompts for a client; `--client codex` is optional. No client is
39
+ selected by default.
38
40
 
39
41
  ## Requirements and first use
40
42
 
@@ -46,13 +48,10 @@ fetch an unverified package now. With a local installation use
46
48
  MFA factors are currently refused as unsupported.
47
49
  - Free callback port **5173** on localhost. Stop a conflicting development server
48
50
  through its own terminal before setup; the connector never terminates it.
49
- - The deployment's **public anon/publishable Supabase key**, supplied using the
50
- existing `TRACKQA_PUBLIC_KEY` environment variable. `VITE_SUPABASE_ANON_KEY` is an
51
- alternative. Obtain this public value from the deployment operator; never use a
52
- service-role key, database password, OAuth token or MCP bearer token.
51
+ - Official hosted TrackQA's validated **public anon key is included**. No manual
52
+ `TRACKQA_PUBLIC_KEY` setup, application checkout or `.env` file is required.
53
53
 
54
54
  ```powershell
55
- $env:TRACKQA_PUBLIC_KEY = '<deployment-public-anon-or-publishable-key>'
56
55
  npm exec --offline -- trackqa connect --client codex
57
56
  ```
58
57
 
@@ -60,6 +59,24 @@ The installed package uses its included shared runtime helpers; it does not need
60
59
  the application checkout or its environment files. Source-checkout fallback to the
61
60
  app's public-key setting is development-only. No `.env` file is packaged or created.
62
61
 
62
+ ### Advanced: custom public configuration
63
+
64
+ The connector continues to pin its backend URL. A custom deployment uses a custom
65
+ build pinned to its own backend, with a matching public key supplied through
66
+ `TRACKQA_PUBLIC_KEY` or, alternatively, `VITE_SUPABASE_ANON_KEY`. A key override
67
+ alone does not change the destination. No runtime URL override was added.
68
+
69
+ ```powershell
70
+ $env:TRACKQA_PUBLIC_KEY = '<matching-deployment-public-anon-or-publishable-key>'
71
+ ```
72
+
73
+ Explicit environment values take precedence over the development-file fallback
74
+ and official default. Invalid explicit values fail closed. Legacy anon JWTs with
75
+ a different project reference are rejected; opaque publishable keys cannot be
76
+ associated with a deployment offline and must be validated by that backend.
77
+ The official default is never used for a different pinned deployment. Never use
78
+ a secret/service-role key, database password, Owner session or MCP bearer token.
79
+
63
80
  Select one client and one project, inspect the token-free preview, and approve
64
81
  installation, project MCP enablement if needed, and one scoped connection. The
65
82
  connector launches the client with the credential only in the child environment.
@@ -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
- }
@@ -4,6 +4,10 @@ import { parseEnv } from 'node:util';
4
4
  import { googlePkceSignIn, GooglePkceError } from '../../mcp-pilot/google-pkce.mjs';
5
5
  import { boundedJson, ConnectorError, requireValue, SUPABASE_URL, UUID } from "./core.js";
6
6
  import { fixtureMembershipRoute, fixtureRequestAllowed, fixtureScopeState, observeFixtureResponse } from "./testing/copilot-fixture-scope.js";
7
+ // Public browser configuration, paired with the official hosted project in the
8
+ // app's trusted VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY configuration.
9
+ // This is not an Owner session, MCP bearer credential, or service-role key.
10
+ export const OFFICIAL_PUBLIC_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im16ZXVpbXFmcnN3cndvYXFqZG1wIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODgzMTUxMzMsImV4cCI6MjEwMzg5MTEzM30.w12klp31YCC3bydpI07QnewBoEi3pv6qFG0xxsMDOyY";
7
11
  export function isPublicKey(key) {
8
12
  if (/^sb_publishable_[A-Za-z0-9_-]+$/.test(key))
9
13
  return true;
@@ -14,11 +18,22 @@ export function isPublicKey(key) {
14
18
  return false;
15
19
  }
16
20
  }
17
- export async function publicKey(env = process.env) {
21
+ function keyMatchesDeployment(key, deploymentUrl) {
22
+ try {
23
+ const claims = JSON.parse(Buffer.from(key.split('.')[1], 'base64url').toString());
24
+ // Legacy hosted anon keys identify their project. Opaque publishable keys
25
+ // cannot be associated offline; the destination still validates them.
26
+ return claims.ref === undefined || deploymentUrl === `https://${claims.ref}.supabase.co`;
27
+ }
28
+ catch {
29
+ return key.startsWith('sb_publishable_');
30
+ }
31
+ }
32
+ export async function publicKey(env = process.env, deploymentUrl = SUPABASE_URL) {
18
33
  let value = env.TRACKQA_PUBLIC_KEY ?? env.VITE_SUPABASE_ANON_KEY;
19
34
  // Local development only: read the existing app's public key, never load its
20
35
  // other variables into process.env or pass them to the coding client.
21
- if (!value) {
36
+ if (value === undefined) {
22
37
  for (const file of ['.env.local', '.env']) {
23
38
  try {
24
39
  const text = await readFile(new URL('../../../' + file, import.meta.url), 'utf8');
@@ -31,13 +46,19 @@ export async function publicKey(env = process.env) {
31
46
  catch { /* No secret or filesystem error details leave this function. */ }
32
47
  }
33
48
  }
49
+ // Never send the official default to another deployment. A custom build must
50
+ // supply its own public key; invalid explicit overrides never fall back.
51
+ if (value === undefined && keyMatchesDeployment(OFFICIAL_PUBLIC_KEY, deploymentUrl))
52
+ value = OFFICIAL_PUBLIC_KEY;
34
53
  requireValue(value && value.length <= 8192 && isPublicKey(value), 'public_key_required');
54
+ requireValue(keyMatchesDeployment(value, deploymentUrl), 'public_key_deployment_mismatch');
35
55
  return value;
36
56
  }
37
57
  // The CLI receives only a checked human API closure. Raw Auth tokens and the SDK
38
58
  // client never reach the client adapter or the recovery store.
39
59
  export async function withOwner(options, action) {
40
60
  requireValue(isPublicKey(options.key), 'public_key_required');
61
+ requireValue(keyMatchesDeployment(options.key, SUPABASE_URL), 'public_key_deployment_mismatch');
41
62
  if (options.fixtureScope)
42
63
  fixtureScopeState(options.fixtureScope);
43
64
  const posts = new Set();
@@ -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.0 - 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
@@ -34,21 +34,38 @@ VS Code optional: --vscode-path <trusted installed absolute Code.exe>; supported
34
34
  Claude connect only: --claude-only excludes other MCP servers for this session (opt-in).
35
35
  No client is selected by default. AI-client accounts are separate from TrackQA sign-in.
36
36
  Requires Node >=22.19, Windows, a free localhost callback port 5173 and Google Owner authorization; MFA accounts are unsupported.
37
- Set TRACKQA_PUBLIC_KEY to the deployment's public anon/publishable key. Never supply a secret or service-role key.
37
+ Official hosted TrackQA public configuration is included; no TRACKQA_PUBLIC_KEY setup is required.
38
+ Advanced: TRACKQA_PUBLIC_KEY or VITE_SUPABASE_ANON_KEY overrides the public key for the build's pinned backend. Never use a secret or service-role key.
38
39
  No bearer token is saved. The existing 90-day token expiry is unchanged; session cleanup attempts and independently verifies revocation.
39
40
  Status is local-only: it does not authorize or test a live MCP connection.
40
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
+ }
41
54
  export async function locations(env = process.env, client = 'codex') {
42
55
  const codexHome = env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
43
56
  if (client === 'codex')
44
57
  requireValue(path.isAbsolute(codexHome), 'configuration_location_untrusted');
45
58
  const canonical = await realpath(codexHome).catch(() => path.resolve(codexHome));
46
59
  const repo = fileURLToPath(new URL('../../../', import.meta.url));
47
- 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()));
48
62
  if (client === 'codex')
49
- 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');
50
64
  const data = process.platform === 'win32' ? env.LOCALAPPDATA : env.XDG_STATE_HOME ?? path.join(os.homedir(), '.local', 'state');
51
- 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');
52
69
  const geminiHome = env.GEMINI_CLI_HOME ?? os.homedir();
53
70
  const geminiCanonical = await realpath(geminiHome).catch(() => path.resolve(geminiHome));
54
71
  if (client === 'gemini') {
@@ -61,6 +78,8 @@ export async function locations(env = process.env, client = 'codex') {
61
78
  }
62
79
  const codes = new Set(`usage configuration_busy configuration_write_failed configuration_write_uncertain gemini_auto_update_enabled gemini_compatibility_unverified client_unsupported client_foreground_unsupported client_not_found executable_untrusted executable_changed public_key_required authentication_failed callback_port_in_use callback_unavailable callback_timeout callback_rejected oauth_denied oauth_failed browser_open_failed interrupted identity_unverified profile_required mfa_unsupported owner_required no_eligible_owner_projects project_unavailable project_selection_unavailable request_failed response_invalid response_limit signout_unconfirmed configuration_unreadable configuration_conflict configuration_changed configuration_home_mismatch configuration_location_untrusted installation_unconfirmed removal_unconfirmed recovery_invalid recovery_unavailable existing_connection_recover_first connector_busy_or_stale_lock enablement_unconfirmed issuance_unconfirmed token_invalid cleanup_unconfirmed no_managed_connections client_command_failed client_command_timeout client_response_limit client_launch_failed client_exit_failed verification_initialization_failed verification_discovery_failed verification_connection_status_failed verification_project_scope_failed`.split(' '));
63
80
  export function safeCode(error) {
81
+ if (error instanceof ConnectorError && error.code === 'public_key_deployment_mismatch')
82
+ return error.code;
64
83
  if (error instanceof ConnectorError && ['copilot_runtime_unverified', 'copilot_process_unconfirmed'].includes(error.code))
65
84
  return error.code;
66
85
  if (error instanceof ConnectorError && error.code === 'vscode_process_unconfirmed')
@@ -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.0' } });
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.0",
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" },