trackqa 0.0.0-stage → 0.1.1

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 +175 -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 +174 -0
  5. package/dist/tools/trackqa-connect/src/cli.js +249 -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,176 @@
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
+ **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.
14
+
15
+ To verify an approved local tarball in a disposable directory:
16
+
17
+ ```powershell
18
+ npm install --ignore-scripts --no-audit --no-fund <absolute-path-to-trackqa-0.1.1.tgz>
19
+ npm exec --offline -- trackqa --help
20
+ ```
21
+
22
+ After v0.1.1 is approved and published, the official hosted workflow is:
23
+
24
+ ```powershell
25
+ npx trackqa connect
26
+ # Explicit client selection is also supported:
27
+ npx trackqa connect --client codex
28
+ npx trackqa status --client codex
29
+ npx trackqa disconnect --client codex --project <project-UUID>
30
+ ```
31
+
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 ...`.
35
+ Bare `connect` prompts for a client; `--client codex` is optional. No client is
36
+ selected by default.
37
+
38
+ ## Requirements and first use
39
+
40
+ - Windows, Node.js **22.19 or newer**, npm and an interactive terminal.
41
+ - An independently installed, compatible Codex CLI and its own account access.
42
+ The connector never installs, upgrades or signs into an AI client automatically.
43
+ - A TrackQA account with a profile and **OWNER** membership in the selected project.
44
+ Google PKCE sign-in is interactive in the system browser. Accounts with verified
45
+ MFA factors are currently refused as unsupported.
46
+ - Free callback port **5173** on localhost. Stop a conflicting development server
47
+ through its own terminal before setup; the connector never terminates it.
48
+ - Official hosted TrackQA's validated **public anon key is included**. No manual
49
+ `TRACKQA_PUBLIC_KEY` setup, application checkout or `.env` file is required.
50
+
51
+ ```powershell
52
+ npm exec --offline -- trackqa connect --client codex
53
+ ```
54
+
55
+ The installed package uses its included shared runtime helpers; it does not need
56
+ the application checkout or its environment files. Source-checkout fallback to the
57
+ app's public-key setting is development-only. No `.env` file is packaged or created.
58
+
59
+ ### Advanced: custom public configuration
60
+
61
+ The connector continues to pin its backend URL. A custom deployment uses a custom
62
+ build pinned to its own backend, with a matching public key supplied through
63
+ `TRACKQA_PUBLIC_KEY` or, alternatively, `VITE_SUPABASE_ANON_KEY`. A key override
64
+ alone does not change the destination. No runtime URL override was added.
65
+
66
+ ```powershell
67
+ $env:TRACKQA_PUBLIC_KEY = '<matching-deployment-public-anon-or-publishable-key>'
68
+ ```
69
+
70
+ Explicit environment values take precedence over the development-file fallback
71
+ and official default. Invalid explicit values fail closed. Legacy anon JWTs with
72
+ a different project reference are rejected; opaque publishable keys cannot be
73
+ associated with a deployment offline and must be validated by that backend.
74
+ The official default is never used for a different pinned deployment. Never use
75
+ a secret/service-role key, database password, Owner session or MCP bearer token.
76
+
77
+ Select one client and one project, inspect the token-free preview, and approve
78
+ installation, project MCP enablement if needed, and one scoped connection. The
79
+ connector launches the client with the credential only in the child environment.
80
+ TrackQA Owner sign-in and any Codex/GitHub/Google/Claude AI-account sign-in are
81
+ separate. Normal interactive tool approvals remain in effect.
82
+
83
+ ## Support and validation limits
84
+
85
+ | Client | Status |
86
+ | --- | --- |
87
+ | Codex CLI | **MVP-supported candidate**, Windows and Owner-operated. Hosted initialization, five tools, scoped reads, foreground launch, revocation and cleanup passed. |
88
+ | VS Code Local (`vscode`) | **Experimental**. Windows hosted read-only acceptance passed. Manual MCP activation and same-session model access are required. |
89
+ | 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. |
90
+ | Gemini CLI | **Experimental; hosted validation deferred**. Local/native configuration checks passed; native Google authentication remains blocked. |
91
+ | Claude Code | **Experimental; hosted validation deferred**. Native installation/configuration flags checked; test-account eligibility blocked authenticated acceptance. |
92
+ | Antigravity | **Deferred**; no adapter. Safe isolation remains unverified. |
93
+
94
+ VS Code detection currently supports Windows stable 1.143.x. Other platforms,
95
+ portable/forked clients, additional providers and parallel multi-client sessions
96
+ are outside this MVP. Use `trackqa --help` for supported executable-path flags.
97
+ Claims about a client apply only to the validated scope above.
98
+
99
+ On **November 4, 2026**, the isolated Copilot run against MCP Synthetic Pilot reached
100
+ Running, discovered five tools and successfully called `trackqa_connection_status`,
101
+ `trackqa_list_projects`, `trackqa_list_issues` and `trackqa_get_issue` once each. Four
102
+ native Search Tools operations were permitted; no unrelated application tools,
103
+ delegated agents or `trackqa_submit_finding` invocation were recorded. Project
104
+ isolation, revocation, fixture restoration, unchanged inventories and owned MCP
105
+ configuration removal were verified. The credential-pattern scan covered 166 files
106
+ with zero matches or exclusions.
107
+
108
+ Write-tool execution, authentication persistence, normal editor exit and complete
109
+ detached Agent Host/SDK termination remain unvalidated. The status response reported
110
+ `read_only=false` and `issue_writes_allowed=false`: the acceptance procedure used
111
+ only reads; the existing token's finding capability was not removed. Findings still
112
+ follow TrackQA's existing human QA review workflow. Ordinary Copilot activation is
113
+ not enabled by these results. Hosted acceptance scripts are not shipped.
114
+
115
+ ## Credentials, disconnect and recovery
116
+
117
+ Owner PKCE/session credentials remain in memory and local sign-out is attempted on
118
+ exit. MCP tokens are never placed in arguments, configuration, logs or recovery
119
+ records. Configuration contains only the literal environment reference. Other
120
+ trusted processes on the same OS account may inspect a child's environment.
121
+
122
+ **The existing 90-day token expiry is unchanged.** This is session-based delivery,
123
+ not a short-lived token guarantee. Normal exit, handled interruption or explicit
124
+ Disconnect attempts revocation and independently checks the server record. A hard
125
+ kill, power loss or network failure can prevent cleanup; local configuration removal
126
+ alone does not revoke access.
127
+
128
+ Recovery metadata is stored in `%LOCALAPPDATA%\TrackQA-Connect`, outside the checkout.
129
+ After an interrupted session:
130
+
131
+ ```powershell
132
+ npm exec --offline -- trackqa status --client codex
133
+ npm exec --offline -- trackqa disconnect --client codex --project <project-UUID>
134
+ ```
135
+
136
+ `status` is local-only and does not prove authorization or connectivity. Disconnect
137
+ may require fresh Google Owner sign-in. Failed or uncertain issuance/revocation
138
+ retains actionable recovery metadata; do not bypass it or issue another connection
139
+ automatically. An Owner can also revoke the labeled connection in TrackQA's existing
140
+ AI Integrations → Manage Connections controls.
141
+
142
+ Only unchanged connector-owned configuration is removed. Edited entries and
143
+ unexpected files are preserved. Remove a stale `connector.lock` only after confirming
144
+ its recorded process has exited. Do not delete pending recovery evidence.
145
+
146
+ Normal session cleanup does **not** silently disable project-wide MCP or revoke
147
+ other users' connections. Any project enablement is persistent and explicitly
148
+ approved. Synthetic acceptance restoration was a separate, authorized Owner action.
149
+
150
+ ## Package contents and source development
151
+
152
+ The artifact contains this README, package metadata and the exact compiled runtime
153
+ dependency closure. It includes the existing PKCE/shared MCP helper and the internal
154
+ acceptance-capability guard required by authentication/workflow imports. That guard
155
+ is not an acceptance runner or a synthetic server. Its behavior is unchanged.
156
+
157
+ Source, tests, hosted/synthetic runners, fixture servers, development tooling,
158
+ obsolete WMI supervision modules, environment files and runtime profiles are excluded.
159
+ No install scripts run on consumers. The declared runtime dependency is
160
+ `@supabase/supabase-js@2.113.0`; build tooling stays in the source checkout.
161
+
162
+ Source developers use the repository's existing dependencies:
163
+
164
+ ```powershell
165
+ npm --prefix tools/trackqa-connect run check
166
+ npm --prefix tools/trackqa-connect run lint
167
+ npm --prefix tools/trackqa-connect run build
168
+ npm --prefix tools/trackqa-connect test
169
+ ```
170
+
171
+ Build clears only the verified owned `dist` directory, compiles the CLI and shared
172
+ dependencies, and checks the exact runtime allowlist. `prepack` repeats that clean
173
+ build when packing from the source checkout. Preserve generated tarballs/checksums
174
+ under the ignored `artifacts/` directory. Historical implementation and supervised
175
+ acceptance procedures remain in source-only `DEVELOPMENT.md`; no new hosted run or
176
+ 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
+ }