trackqa 0.0.0-stage → 0.1.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.
Files changed (26) hide show
  1. package/README.md +161 -2
  2. package/dist/src/lib/mcpOnboarding.js +143 -0
  3. package/dist/tools/mcp-pilot/google-pkce.mjs +244 -0
  4. package/dist/tools/trackqa-connect/src/auth.js +153 -0
  5. package/dist/tools/trackqa-connect/src/cli.js +246 -0
  6. package/dist/tools/trackqa-connect/src/clients/adapter.js +3 -0
  7. package/dist/tools/trackqa-connect/src/clients/claude-session.js +78 -0
  8. package/dist/tools/trackqa-connect/src/clients/claude.js +113 -0
  9. package/dist/tools/trackqa-connect/src/clients/codex-verify.js +110 -0
  10. package/dist/tools/trackqa-connect/src/clients/codex.js +191 -0
  11. package/dist/tools/trackqa-connect/src/clients/gemini-config.js +141 -0
  12. package/dist/tools/trackqa-connect/src/clients/gemini-verify.js +14 -0
  13. package/dist/tools/trackqa-connect/src/clients/gemini-windows.js +134 -0
  14. package/dist/tools/trackqa-connect/src/clients/gemini.js +249 -0
  15. package/dist/tools/trackqa-connect/src/clients/vscode-copilot-lifecycle.js +97 -0
  16. package/dist/tools/trackqa-connect/src/clients/vscode-copilot-session.js +115 -0
  17. package/dist/tools/trackqa-connect/src/clients/vscode-copilot.js +59 -0
  18. package/dist/tools/trackqa-connect/src/clients/vscode-process.js +123 -0
  19. package/dist/tools/trackqa-connect/src/clients/vscode-session.js +145 -0
  20. package/dist/tools/trackqa-connect/src/clients/vscode.js +123 -0
  21. package/dist/tools/trackqa-connect/src/core.js +87 -0
  22. package/dist/tools/trackqa-connect/src/process.js +76 -0
  23. package/dist/tools/trackqa-connect/src/state.js +80 -0
  24. package/dist/tools/trackqa-connect/src/testing/copilot-fixture-scope.js +103 -0
  25. package/dist/tools/trackqa-connect/src/workflow.js +241 -0
  26. package/package.json +43 -4
package/README.md CHANGED
@@ -1,3 +1,162 @@
1
- # Temporary Holding Version
1
+ # TrackQA Universal Connector — Windows MVP release candidate
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Connect an existing AI coding client to TrackQA's remote MCP server without copying
4
+ bearer tokens or editing MCP configuration manually. Codex CLI is the MVP-supported
5
+ candidate. Setup is Owner-operated, with explicit project approval and session-based
6
+ credentials. The connector does not install or authenticate the AI client itself.
7
+
8
+ ## Package status and installation
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.
17
+
18
+ To verify an approved local tarball in a disposable directory:
19
+
20
+ ```powershell
21
+ npm install --ignore-scripts --no-audit --no-fund <absolute-path-to-trackqa-0.1.0.tgz>
22
+ npm exec --offline -- trackqa --help
23
+ ```
24
+
25
+ After the intended package is approved and published, the target workflow is:
26
+
27
+ ```powershell
28
+ npx trackqa connect
29
+ # Explicit client selection is also supported:
30
+ npx trackqa connect --client codex
31
+ npx trackqa status --client codex
32
+ npx trackqa disconnect --client codex --project <project-UUID>
33
+ ```
34
+
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.
38
+
39
+ ## Requirements and first use
40
+
41
+ - Windows, Node.js **22.19 or newer**, npm and an interactive terminal.
42
+ - An independently installed, compatible Codex CLI and its own account access.
43
+ The connector never installs, upgrades or signs into an AI client automatically.
44
+ - A TrackQA account with a profile and **OWNER** membership in the selected project.
45
+ Google PKCE sign-in is interactive in the system browser. Accounts with verified
46
+ MFA factors are currently refused as unsupported.
47
+ - Free callback port **5173** on localhost. Stop a conflicting development server
48
+ 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.
53
+
54
+ ```powershell
55
+ $env:TRACKQA_PUBLIC_KEY = '<deployment-public-anon-or-publishable-key>'
56
+ npm exec --offline -- trackqa connect --client codex
57
+ ```
58
+
59
+ The installed package uses its included shared runtime helpers; it does not need
60
+ the application checkout or its environment files. Source-checkout fallback to the
61
+ app's public-key setting is development-only. No `.env` file is packaged or created.
62
+
63
+ Select one client and one project, inspect the token-free preview, and approve
64
+ installation, project MCP enablement if needed, and one scoped connection. The
65
+ connector launches the client with the credential only in the child environment.
66
+ TrackQA Owner sign-in and any Codex/GitHub/Google/Claude AI-account sign-in are
67
+ separate. Normal interactive tool approvals remain in effect.
68
+
69
+ ## Support and validation limits
70
+
71
+ | Client | Status |
72
+ | --- | --- |
73
+ | Codex CLI | **MVP-supported candidate**, Windows and Owner-operated. Hosted initialization, five tools, scoped reads, foreground launch, revocation and cleanup passed. |
74
+ | VS Code Local (`vscode`) | **Experimental**. Windows hosted read-only acceptance passed. Manual MCP activation and same-session model access are required. |
75
+ | VS Code Copilot (`vscode-copilot`) | **Experimental and guarded**. Hosted read-only acceptance passed through the separate fixture-pinned test entry. Ordinary `connect` still stops before Owner authentication, issuance or launch. |
76
+ | Gemini CLI | **Experimental; hosted validation deferred**. Local/native configuration checks passed; native Google authentication remains blocked. |
77
+ | Claude Code | **Experimental; hosted validation deferred**. Native installation/configuration flags checked; test-account eligibility blocked authenticated acceptance. |
78
+ | Antigravity | **Deferred**; no adapter. Safe isolation remains unverified. |
79
+
80
+ VS Code detection currently supports Windows stable 1.143.x. Other platforms,
81
+ portable/forked clients, additional providers and parallel multi-client sessions
82
+ are outside this MVP. Use `trackqa --help` for supported executable-path flags.
83
+ Claims about a client apply only to the validated scope above.
84
+
85
+ On **November 4, 2026**, the isolated Copilot run against MCP Synthetic Pilot reached
86
+ Running, discovered five tools and successfully called `trackqa_connection_status`,
87
+ `trackqa_list_projects`, `trackqa_list_issues` and `trackqa_get_issue` once each. Four
88
+ native Search Tools operations were permitted; no unrelated application tools,
89
+ delegated agents or `trackqa_submit_finding` invocation were recorded. Project
90
+ isolation, revocation, fixture restoration, unchanged inventories and owned MCP
91
+ configuration removal were verified. The credential-pattern scan covered 166 files
92
+ with zero matches or exclusions.
93
+
94
+ Write-tool execution, authentication persistence, normal editor exit and complete
95
+ detached Agent Host/SDK termination remain unvalidated. The status response reported
96
+ `read_only=false` and `issue_writes_allowed=false`: the acceptance procedure used
97
+ only reads; the existing token's finding capability was not removed. Findings still
98
+ follow TrackQA's existing human QA review workflow. Ordinary Copilot activation is
99
+ not enabled by these results. Hosted acceptance scripts are not shipped.
100
+
101
+ ## Credentials, disconnect and recovery
102
+
103
+ Owner PKCE/session credentials remain in memory and local sign-out is attempted on
104
+ exit. MCP tokens are never placed in arguments, configuration, logs or recovery
105
+ records. Configuration contains only the literal environment reference. Other
106
+ trusted processes on the same OS account may inspect a child's environment.
107
+
108
+ **The existing 90-day token expiry is unchanged.** This is session-based delivery,
109
+ not a short-lived token guarantee. Normal exit, handled interruption or explicit
110
+ Disconnect attempts revocation and independently checks the server record. A hard
111
+ kill, power loss or network failure can prevent cleanup; local configuration removal
112
+ alone does not revoke access.
113
+
114
+ Recovery metadata is stored in `%LOCALAPPDATA%\TrackQA-Connect`, outside the checkout.
115
+ After an interrupted session:
116
+
117
+ ```powershell
118
+ npm exec --offline -- trackqa status --client codex
119
+ npm exec --offline -- trackqa disconnect --client codex --project <project-UUID>
120
+ ```
121
+
122
+ `status` is local-only and does not prove authorization or connectivity. Disconnect
123
+ may require fresh Google Owner sign-in. Failed or uncertain issuance/revocation
124
+ retains actionable recovery metadata; do not bypass it or issue another connection
125
+ automatically. An Owner can also revoke the labeled connection in TrackQA's existing
126
+ AI Integrations → Manage Connections controls.
127
+
128
+ Only unchanged connector-owned configuration is removed. Edited entries and
129
+ unexpected files are preserved. Remove a stale `connector.lock` only after confirming
130
+ its recorded process has exited. Do not delete pending recovery evidence.
131
+
132
+ Normal session cleanup does **not** silently disable project-wide MCP or revoke
133
+ other users' connections. Any project enablement is persistent and explicitly
134
+ approved. Synthetic acceptance restoration was a separate, authorized Owner action.
135
+
136
+ ## Package contents and source development
137
+
138
+ The artifact contains this README, package metadata and the exact compiled runtime
139
+ dependency closure. It includes the existing PKCE/shared MCP helper and the internal
140
+ acceptance-capability guard required by authentication/workflow imports. That guard
141
+ is not an acceptance runner or a synthetic server. Its behavior is unchanged.
142
+
143
+ Source, tests, hosted/synthetic runners, fixture servers, development tooling,
144
+ obsolete WMI supervision modules, environment files and runtime profiles are excluded.
145
+ No install scripts run on consumers. The declared runtime dependency is
146
+ `@supabase/supabase-js@2.113.0`; build tooling stays in the source checkout.
147
+
148
+ Source developers use the repository's existing dependencies:
149
+
150
+ ```powershell
151
+ npm --prefix tools/trackqa-connect run check
152
+ npm --prefix tools/trackqa-connect run lint
153
+ npm --prefix tools/trackqa-connect run build
154
+ npm --prefix tools/trackqa-connect test
155
+ ```
156
+
157
+ Build clears only the verified owned `dist` directory, compiles the CLI and shared
158
+ dependencies, and checks the exact runtime allowlist. `prepack` repeats that clean
159
+ build when packing from the source checkout. Preserve generated tarballs/checksums
160
+ under the ignored `artifacts/` directory. Historical implementation and supervised
161
+ acceptance procedures remain in source-only `DEVELOPMENT.md`; no new hosted run or
162
+ publication is authorized by this documentation.
@@ -0,0 +1,143 @@
1
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2
+ export const connectionFields = 'id,project_id,user_id,approved_by,client_label,token_expires_at,created_at,updated_at,revoked_at';
3
+ export const mcpTools = [
4
+ { name: 'trackqa_connection_status', description: 'Check the authorized connection and project.' },
5
+ { name: 'trackqa_list_projects', description: 'Show the single approved project.' },
6
+ { name: 'trackqa_list_issues', description: 'List issues in that project.' },
7
+ { name: 'trackqa_get_issue', description: 'Read an issue in that project.' },
8
+ { name: 'trackqa_submit_finding', description: 'Submit a potential bug for human QA/Owner review.' },
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.
16
+ export function mcpSetup(supabaseUrl, projectId) {
17
+ let url;
18
+ try {
19
+ url = new URL(supabaseUrl);
20
+ }
21
+ catch {
22
+ throw new Error('MCP endpoint configuration is unavailable.');
23
+ }
24
+ if (url.protocol !== 'https:' || url.username || url.password || url.pathname !== '/'
25
+ || url.search || url.hash || url.port || !uuid.test(projectId)) {
26
+ throw new Error('MCP endpoint configuration is unavailable.');
27
+ }
28
+ const id = projectId.replaceAll('-', '').toLowerCase();
29
+ return { endpoint: `${url.origin}/functions/v1/trackqa-mcp`, serverName: `trackqa_${id}`,
30
+ envVar: `TRACKQA_MCP_${id.toUpperCase()}_TOKEN`, projectId: projectId.toLowerCase() };
31
+ }
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
+ export function connectionStatus(connection, enabled, archived, memberPresent, now) {
88
+ if (connection.revoked_at)
89
+ return 'Revoked';
90
+ const expires = Date.parse(connection.token_expires_at);
91
+ if (!Number.isFinite(expires) || expires <= now)
92
+ return 'Expired';
93
+ if (!connection.user_id || !connection.approved_by || !memberPresent)
94
+ return 'Unavailable';
95
+ if (archived)
96
+ return 'Project archived';
97
+ if (!enabled)
98
+ return 'Project disabled';
99
+ return 'Ready to connect';
100
+ }
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
+ }
@@ -0,0 +1,244 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
3
+ import { execFile } from 'node:child_process';
4
+ import path from 'node:path';
5
+ export const GOOGLE_REDIRECT = 'http://localhost:5173/mcp-pilot/callback';
6
+ export const GOOGLE_CODES = Object.freeze(['callback_port_in_use', 'callback_unavailable',
7
+ 'callback_timeout', 'callback_rejected', 'oauth_denied', 'oauth_failed',
8
+ 'browser_open_failed', 'interrupted']);
9
+ const HOST = 'localhost:5173';
10
+ const CALLBACK = '/mcp-pilot/callback';
11
+ const COOKIE = 'trackqa_mcp_pilot_flow';
12
+ const START = '/mcp-pilot/start';
13
+ export class GooglePkceError extends Error {
14
+ constructor(code) {
15
+ super('Google authorization did not complete.');
16
+ this.code = GOOGLE_CODES.includes(code) ? code : 'oauth_failed';
17
+ }
18
+ }
19
+ const failure = code => new GooglePkceError(code);
20
+ // Only this local, one-time bootstrap URL reaches the default browser launcher.
21
+ // It contains no authorization code, access/refresh token or PKCE verifier.
22
+ export function openSystemBrowser(url) {
23
+ const u = new URL(url);
24
+ if (process.platform !== 'win32' || u.origin !== 'http://' + HOST || u.pathname !== START
25
+ || !/^[0-9a-f]{64}$/.test(u.searchParams.get('nonce') ?? ''))
26
+ throw failure('browser_open_failed');
27
+ return new Promise((resolve, reject) => {
28
+ execFile(path.join(process.env.SystemRoot ?? 'C:/Windows', 'System32/rundll32.exe'), ['url.dll,FileProtocolHandler', u.href], { windowsHide: true, timeout: 5000, maxBuffer: 1024 }, error => error ? reject(failure('browser_open_failed')) : resolve());
29
+ });
30
+ }
31
+ // No request logger. Two fixed loopback bindings prevent localhost resolving to
32
+ // a different application's IPv4/IPv6 listener. Never use 0.0.0.0 or :: here.
33
+ export async function createGoogleCallback({ timeoutMs = 300_000, signal, serverFactory = createServer } = {}) {
34
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000)
35
+ throw failure('callback_unavailable');
36
+ const nonce = randomBytes(32).toString('hex');
37
+ const servers = [];
38
+ let authorizationUrl;
39
+ let flowId;
40
+ let started = false;
41
+ let settled = false;
42
+ let closed = false;
43
+ let timer;
44
+ let resolveCode, rejectCode;
45
+ const code = new Promise((resolve, reject) => { resolveCode = resolve; rejectCode = reject; });
46
+ // A failure can occur while opening the browser, before the caller awaits it.
47
+ void code.catch(() => { });
48
+ function finish(error, value) {
49
+ if (settled)
50
+ return;
51
+ settled = true;
52
+ clearTimeout(timer);
53
+ error ? rejectCode(error) : resolveCode(value);
54
+ }
55
+ const abort = () => finish(failure('interrupted'));
56
+ async function close() {
57
+ if (closed)
58
+ return;
59
+ closed = true;
60
+ clearTimeout(timer);
61
+ signal?.removeEventListener('abort', abort);
62
+ if (!settled)
63
+ finish(failure('interrupted'));
64
+ await Promise.all(servers.map(server => new Promise(resolve => {
65
+ const force = setTimeout(() => server.closeAllConnections?.(), 250);
66
+ try {
67
+ server.close(() => { clearTimeout(force); resolve(); });
68
+ server.closeIdleConnections?.();
69
+ }
70
+ catch {
71
+ clearTimeout(force);
72
+ resolve();
73
+ }
74
+ })));
75
+ authorizationUrl = undefined;
76
+ flowId = undefined;
77
+ }
78
+ function respond(response, status, message, extra = {}) {
79
+ response.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8',
80
+ 'Cache-Control': 'no-store', 'Pragma': 'no-cache', 'Referrer-Policy': 'no-referrer',
81
+ 'Content-Security-Policy': "default-src 'none'; frame-ancestors 'none'", Connection: 'close', ...extra });
82
+ response.end(message);
83
+ }
84
+ const clearCookie = `${COOKIE}=; Path=${CALLBACK}; HttpOnly; SameSite=Lax; Max-Age=0`;
85
+ function handler(request, response) {
86
+ try {
87
+ const remote = request.socket.remoteAddress;
88
+ if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(remote)
89
+ || request.headers.host !== HOST || (request.headers.origin && request.headers.origin !== 'http://' + HOST)) {
90
+ respond(response, 403, 'Request rejected.');
91
+ return;
92
+ }
93
+ if (request.method !== 'GET') {
94
+ respond(response, 405, 'Request rejected.');
95
+ return;
96
+ }
97
+ const raw = request.url ?? '';
98
+ if (!raw.startsWith('/') || raw.startsWith('//') || raw.length > 8192) {
99
+ respond(response, 400, 'Request rejected.');
100
+ return;
101
+ }
102
+ if (![START, CALLBACK].includes(raw.split('?')[0])) {
103
+ respond(response, 404, 'Not found.');
104
+ return;
105
+ }
106
+ const url = new URL(raw, 'http://' + HOST);
107
+ if (url.hash) {
108
+ respond(response, 400, 'Request rejected.');
109
+ return;
110
+ }
111
+ if (url.pathname === START) {
112
+ if (settled || started || !authorizationUrl || url.searchParams.getAll('nonce').length !== 1
113
+ || url.searchParams.get('nonce') !== nonce || [...url.searchParams.keys()].some(k => k !== 'nonce')) {
114
+ respond(response, 403, 'Request rejected.');
115
+ return;
116
+ }
117
+ started = true;
118
+ // Session cookie: no OAuth credentials and no persistent expiry. It is
119
+ // only a browser-to-attempt correlation value for this loopback origin.
120
+ respond(response, 302, 'Continue authorization in the browser.', {
121
+ Location: authorizationUrl,
122
+ 'Set-Cookie': `${COOKIE}=${nonce}; Path=${CALLBACK}; HttpOnly; SameSite=Lax`,
123
+ });
124
+ return;
125
+ }
126
+ if (url.pathname !== CALLBACK) {
127
+ respond(response, 404, 'Not found.');
128
+ return;
129
+ }
130
+ if (settled) {
131
+ respond(response, 410, 'Authorization response already handled.');
132
+ return;
133
+ }
134
+ const cookies = String(request.headers.cookie ?? '').split(';').map(v => v.trim())
135
+ .filter(v => v.startsWith(COOKIE + '=')).map(v => v.slice(COOKIE.length + 1));
136
+ const matched = cookies.length === 1 && /^[0-9a-f]{64}$/.test(cookies[0])
137
+ && timingSafeEqual(Buffer.from(cookies[0]), Buffer.from(nonce));
138
+ const returnedFlow = url.searchParams.getAll('sb_flow_id');
139
+ if (!started || !matched || returnedFlow.length > 1 || (returnedFlow.length === 1 && returnedFlow[0] !== flowId)) {
140
+ respond(response, 400, 'Authorization response rejected.', { 'Set-Cookie': clearCookie });
141
+ finish(failure('callback_rejected'));
142
+ return;
143
+ }
144
+ const allowed = ['code', 'sb_flow_id', 'error', 'error_code', 'error_description'];
145
+ const codes = url.searchParams.getAll('code');
146
+ const hasError = ['error', 'error_code', 'error_description'].some(k => url.searchParams.has(k));
147
+ if ([...url.searchParams.keys()].some(k => !allowed.includes(k) || url.searchParams.getAll(k).length !== 1)
148
+ || codes.length > 1 || (hasError && codes.length !== 0)) {
149
+ respond(response, 400, 'Authorization response rejected.', { 'Set-Cookie': clearCookie });
150
+ finish(failure('callback_rejected'));
151
+ return;
152
+ }
153
+ if (hasError) {
154
+ respond(response, 400, 'Authorization did not complete. Return to the helper.', { 'Set-Cookie': clearCookie });
155
+ finish(failure('oauth_denied'));
156
+ return;
157
+ }
158
+ const value = codes[0];
159
+ if (!value || !/^[A-Za-z0-9._~-]{1,2048}$/.test(value)) {
160
+ respond(response, 400, 'Authorization response rejected.', { 'Set-Cookie': clearCookie });
161
+ finish(failure('callback_rejected'));
162
+ return;
163
+ }
164
+ respond(response, 200, 'Authorization response received. Return to the helper for verification.', { 'Set-Cookie': clearCookie });
165
+ finish(null, value);
166
+ }
167
+ catch {
168
+ try {
169
+ respond(response, 400, 'Authorization response rejected.');
170
+ }
171
+ catch { /* No request/error details logged. */ }
172
+ finish(failure('callback_rejected'));
173
+ }
174
+ }
175
+ try {
176
+ if (signal?.aborted)
177
+ throw failure('interrupted');
178
+ signal?.addEventListener('abort', abort, { once: true });
179
+ for (const host of ['127.0.0.1', '::1']) {
180
+ const server = serverFactory({ maxHeaderSize: 8192, headersTimeout: 5000, requestTimeout: 5000 }, handler);
181
+ server.maxConnections = 4;
182
+ server.keepAliveTimeout = 1000;
183
+ server.on('clientError', (_error, socket) => socket.destroy());
184
+ servers.push(server);
185
+ await new Promise((resolve, reject) => {
186
+ const onError = error => reject(failure(error.code === 'EADDRINUSE' ? 'callback_port_in_use' : 'callback_unavailable'));
187
+ server.once('error', onError);
188
+ server.listen({ host, port: 5173, ipv6Only: true, exclusive: true }, () => {
189
+ server.removeListener('error', onError);
190
+ server.on('error', () => finish(failure('callback_unavailable')));
191
+ resolve();
192
+ });
193
+ });
194
+ }
195
+ if (signal?.aborted)
196
+ throw failure('interrupted');
197
+ timer = setTimeout(() => finish(failure('callback_timeout')), timeoutMs);
198
+ return {
199
+ code, close,
200
+ configure(url, expectedFlow) {
201
+ const u = new URL(url);
202
+ if (authorizationUrl || settled || !/^[A-Za-z0-9_-]{8,64}$/.test(expectedFlow)
203
+ || u.origin !== 'https://mzeuimqfrswrwoaqjdmp.supabase.co' || u.pathname !== '/auth/v1/authorize'
204
+ || u.searchParams.get('provider') !== 'google' || u.searchParams.get('redirect_to') !== GOOGLE_REDIRECT
205
+ || u.searchParams.get('code_challenge_method') !== 's256'
206
+ || !/^[A-Za-z0-9_-]{43}$/.test(u.searchParams.get('code_challenge') ?? ''))
207
+ throw failure('oauth_failed');
208
+ authorizationUrl = u.href;
209
+ flowId = expectedFlow;
210
+ },
211
+ startUrl: 'http://' + HOST + START + '?nonce=' + nonce,
212
+ };
213
+ }
214
+ catch (error) {
215
+ await close();
216
+ throw error instanceof GooglePkceError ? error : failure('callback_unavailable');
217
+ }
218
+ }
219
+ export async function googlePkceSignIn(client, options = {}) {
220
+ const callback = await createGoogleCallback({ timeoutMs: options.oauthTimeoutMs,
221
+ signal: options.signal, serverFactory: options.serverFactory });
222
+ let value, flowId;
223
+ try {
224
+ const start = await client.auth.signInWithOAuth({ provider: 'google',
225
+ options: { redirectTo: GOOGLE_REDIRECT, skipBrowserRedirect: true, queryParams: { prompt: 'select_account' } } });
226
+ if (start.error)
227
+ throw failure('oauth_failed');
228
+ flowId = start.data.flowId;
229
+ callback.configure(start.data.url, flowId);
230
+ const opening = Promise.resolve().then(() => (options.openBrowser ?? openSystemBrowser)(callback.startUrl));
231
+ await Promise.race([opening, callback.code.then(() => undefined)]);
232
+ value = await callback.code;
233
+ }
234
+ catch (error) {
235
+ throw error instanceof GooglePkceError ? error : failure('browser_open_failed');
236
+ }
237
+ finally {
238
+ await callback.close();
239
+ }
240
+ if (options.signal?.aborted)
241
+ throw failure('interrupted');
242
+ // Always select the retained flow's verifier, never a callback-selected slot.
243
+ return client.auth.exchangeCodeForSession(value, { flowId });
244
+ }