pi-git-auth 1.2.2 → 1.2.3

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
@@ -54,6 +54,7 @@ github.ts GitHub REST client
54
54
  gitlab.ts GitLab REST client
55
55
  details.ts read-only repo details overlay (tree + commits + metadata)
56
56
  git-gate.ts command rewriting that injects the token for a host
57
+ git-helpers.ts detection of file-persisting credential helpers (status note)
57
58
  redact.ts secret redaction of tool outputs (PAT / URL-credential patterns)
58
59
  ```
59
60
 
@@ -121,15 +122,28 @@ use that account's token for the account's host (`github.com` or
121
122
 
122
123
  ```
123
124
  export GIT_TERMINAL_PROMPT=0 \
124
- GIT_CONFIG_COUNT=1 \
125
+ GIT_CONFIG_COUNT=2 \
125
126
  GIT_CONFIG_KEY_0="url.https://x-access-token:<token>@<host>/.insteadOf" \
126
- GIT_CONFIG_VALUE_0="https://<host>/" && <command>
127
+ GIT_CONFIG_VALUE_0="https://<host>/" \
128
+ GIT_CONFIG_KEY_1="credential.helper" \
129
+ GIT_CONFIG_VALUE_1="" && <command>
127
130
  ```
128
131
 
129
132
  Forging hosts' git-over-HTTPS endpoints ignore `Authorization` headers
130
133
  and only accept URL-embedded credentials, hence the `insteadOf` rewrite.
131
134
  Only the active host is touched; other remotes are untouched.
132
135
 
136
+ The empty `credential.helper` entry disables git's credential helpers for
137
+ the instrumented process only (env config is read after all file configs,
138
+ and an empty value clears previously defined helpers). This is required so
139
+ that git's post-auth store phase does not persist the injected token — e.g.
140
+ a user-configured `credential.helper = store` would otherwise write it
141
+ plaintext to `~/.git-credentials`. All other standard auth mechanisms
142
+ (URL-embedded credentials, `credential.<url>.*` config, `.netrc`, SSH) are
143
+ unaffected; helpers remain fully active everywhere the gate does not run.
144
+ If a file-persisting helper is detected in the user's config, `/auth
145
+ status` notes it passively (one line, no prompt).
146
+
133
147
  ## Token storage
134
148
 
135
149
  Tokens are **never stored plaintext on disk**. Two backends are
package/auth.ts CHANGED
@@ -117,7 +117,7 @@ export function activeService(data: StoreData): Service | undefined {
117
117
  }
118
118
 
119
119
  /** Human-readable status block: all accounts, details for the active one. */
120
- export function statusDetail(data: StoreData): string {
120
+ export function statusDetail(data: StoreData, opts?: { credHelperSink?: { helper: string; target: string } }): string {
121
121
  const keys = Object.keys(data.accounts);
122
122
  if (keys.length === 0) return "No git accounts. Run /auth login.";
123
123
  const lines = ["Git accounts:"];
@@ -147,6 +147,11 @@ export function statusDetail(data: StoreData): string {
147
147
  storeLine += " (wallet locked on load — token unavailable)";
148
148
  }
149
149
  lines.push(storeLine);
150
+ if (opts?.credHelperSink) {
151
+ lines.push(
152
+ `Git: credential.helper=${opts.credHelperSink.helper} detected — git may also cache gate tokens in ${opts.credHelperSink.target} (the gate itself keeps the token keyring-only)`,
153
+ );
154
+ }
150
155
  if (active && activeKey) {
151
156
  lines.push("");
152
157
  lines.push(`Active: @${active.user ?? activeKey.slice(activeKey.indexOf(":") + 1)} (${active.platform})`);
package/commands.ts CHANGED
@@ -3,6 +3,7 @@ import { loadStore, activeAccount, retryKeyringLoad, type StoreData } from "./st
3
3
  import { SERVICES, type Service, type TreeEntry } from "./forge";
4
4
  import { buildDetailsText, RepoDetailsPanel } from "./details";
5
5
  import { activeService, accountName, loginWithPastedToken, removeAccount, setActiveAccount, statusDetail } from "./auth";
6
+ import { detectCredHelperSink } from "./git-helpers";
6
7
 
7
8
  type Ctx = ExtensionCommandContext;
8
9
 
@@ -12,7 +13,8 @@ async function showStatus(ctx: Ctx): Promise<void> {
12
13
  if (Object.keys(data.accounts).length === 0) {
13
14
  ctx.ui.notify("git auth: not connected — run /auth login", "info");
14
15
  } else {
15
- ctx.ui.notify(statusDetail(data), "info");
16
+ const sink = await detectCredHelperSink(); // cached, fail-silent
17
+ ctx.ui.notify(statusDetail(data, { credHelperSink: sink }), "info");
16
18
  }
17
19
  }
18
20
 
package/git-gate.ts CHANGED
@@ -6,9 +6,11 @@
6
6
  * for that host, regardless of git's own credential configuration:
7
7
  *
8
8
  * export GIT_TERMINAL_PROMPT=0 \
9
- * GIT_CONFIG_COUNT=1 \
9
+ * GIT_CONFIG_COUNT=2 \
10
10
  * GIT_CONFIG_KEY_0="url.https://x-access-token:<token>@<host>/.insteadOf" \
11
- * GIT_CONFIG_VALUE_0="https://<host>/" && <command>
11
+ * GIT_CONFIG_VALUE_0="https://<host>/" \
12
+ * GIT_CONFIG_KEY_1="credential.helper" \
13
+ * GIT_CONFIG_VALUE_1="" && <command>
12
14
  *
13
15
  * Forging hosts' git-over-HTTPS endpoints ignore Authorization headers
14
16
  * and only accept URL-embedded (Basic) credentials, hence the insteadOf
@@ -19,6 +21,14 @@
19
21
  * - GIT_TERMINAL_PROMPT=0: a failed auth surfaces as a clean error
20
22
  * instead of an interactive prompt hanging the TUI.
21
23
  * - Deterministic: no credential-helper races, same behavior every run.
24
+ * - The token is never persisted by git's credential helpers: the empty
25
+ * credential.helper entry (read after all file configs, where an empty
26
+ * value clears previously defined helpers) disables helpers for the
27
+ * instrumented process only. Without it, git's post-auth store phase
28
+ * would hand the injected token to file-based helpers such as `store`,
29
+ * persisting it plaintext to ~/.git-credentials. All other standard
30
+ * auth mechanisms (URL-embedded credentials, credential.<url>.* config,
31
+ * .netrc, SSH) are unaffected.
22
32
  * - SSH-style URLs for the host are rewritten to HTTPS so the token applies.
23
33
  */
24
34
 
@@ -37,8 +47,10 @@ export function instrumentGit(command: string, host: string, token: string): str
37
47
  .replace(new RegExp(`git@${host}:`, "g"), `https://${host}/`);
38
48
  const prefix =
39
49
  `export GIT_TERMINAL_PROMPT=0 ` +
40
- `GIT_CONFIG_COUNT=1 ` +
50
+ `GIT_CONFIG_COUNT=2 ` +
41
51
  `GIT_CONFIG_KEY_0="url.https://x-access-token:${token}@${host}/.insteadOf" ` +
42
- `GIT_CONFIG_VALUE_0="https://${host}/" && `;
52
+ `GIT_CONFIG_VALUE_0="https://${host}/" ` +
53
+ `GIT_CONFIG_KEY_1="credential.helper" ` +
54
+ `GIT_CONFIG_VALUE_1="" && `;
43
55
  return prefix + rewritten;
44
56
  }
package/git-helpers.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Detection of file-persisting git credential helpers.
3
+ *
4
+ * The git gate keeps its token out of helpers by disabling them for the
5
+ * instrumented process (see git-gate.ts). This module is the *advisory*
6
+ * side: it detects whether the user's normal git configuration would have
7
+ * persisted the injected token (e.g. `credential.helper = store` →
8
+ * ~/.git-credentials) so the status view can note it. Passive only — no
9
+ * prompt, no blocking, fail-silent (any error → `undefined`).
10
+ */
11
+ import { execFile } from "node:child_process";
12
+
13
+ export interface CredHelperSink {
14
+ /** The `credential.helper` value as configured (e.g. "store"). */
15
+ helper: string;
16
+ /** Where that helper persists credentials, for the status line. */
17
+ target: string;
18
+ }
19
+
20
+ const TTL_MS = 60_000;
21
+ const TIMEOUT_MS = 3_000;
22
+
23
+ let cache: { at: number; sink: CredHelperSink | undefined } | undefined;
24
+ let inFlight: Promise<CredHelperSink | undefined> | undefined;
25
+
26
+ function sinkFor(value: string): CredHelperSink | undefined {
27
+ const v = value.trim();
28
+ const base = v.split("/").pop() ?? v;
29
+ if (v === "store" || base === "credential-store" || v.endsWith("!store")) {
30
+ return { helper: v, target: "~/.git-credentials (plaintext)" };
31
+ }
32
+ if (v === "netrc" || base === "credential-netrc" || v.endsWith("!netrc")) {
33
+ return { helper: v, target: "~/.netrc" };
34
+ }
35
+ return undefined; // OS wallets (gnome-keyring, osxkeychain, …): fine, no note
36
+ }
37
+
38
+ /**
39
+ * Detect a file-persisting credential helper in the user's normal git
40
+ * config. Cached for 60 s, deduplicated in flight, and fail-silent:
41
+ * missing git, timeout, or no such helper → `undefined`.
42
+ */
43
+ export function detectCredHelperSink(): Promise<CredHelperSink | undefined> {
44
+ const now = Date.now();
45
+ if (cache && now - cache.at < TTL_MS) return Promise.resolve(cache.sink);
46
+ if (inFlight) return inFlight;
47
+ inFlight = new Promise<CredHelperSink | undefined>((resolve) => {
48
+ let done = false;
49
+ const finish = (sink: CredHelperSink | undefined) => {
50
+ if (done) return;
51
+ done = true;
52
+ cache = { at: Date.now(), sink };
53
+ inFlight = undefined;
54
+ resolve(sink);
55
+ };
56
+ const timer = setTimeout(() => finish(undefined), TIMEOUT_MS);
57
+ execFile(
58
+ "git",
59
+ ["config", "--get-all", "--show-origin", "credential.helper"],
60
+ { timeout: TIMEOUT_MS },
61
+ (err, stdout) => {
62
+ clearTimeout(timer);
63
+ if (err) return finish(undefined);
64
+ // Lines: "<origin>\t<value>". Prefer the last (most specific) match.
65
+ let sink: CredHelperSink | undefined;
66
+ for (const line of stdout.split("\n")) {
67
+ const idx = line.indexOf("\t");
68
+ if (idx < 0) continue;
69
+ const found = sinkFor(line.slice(idx + 1));
70
+ if (found) sink = found;
71
+ }
72
+ finish(sink);
73
+ },
74
+ );
75
+ });
76
+ return inFlight;
77
+ }
package/index.ts CHANGED
@@ -5,6 +5,7 @@ import { loadStore, activeAccount, retryKeyringLoad } from "./store";
5
5
  import { SERVICES } from "./forge";
6
6
  import { findAccounts, setActiveAccount, statusDetail } from "./auth";
7
7
  import { instrumentGit } from "./git-gate";
8
+ import { detectCredHelperSink } from "./git-helpers";
8
9
  import { redactSecrets } from "./redact";
9
10
  import { handleAuthCommand } from "./commands";
10
11
 
@@ -90,7 +91,8 @@ export default function (pi: ExtensionAPI) {
90
91
  retryKeyringLoad(); // prompt-safe: recovers the token once the wallet unlocks
91
92
  const data = loadStore();
92
93
  if (Object.keys(data.accounts).length === 0) return { ...text(notConnected), isError: true };
93
- return text(statusDetail(data));
94
+ const sink = await detectCredHelperSink(); // cached, fail-silent
95
+ return text(statusDetail(data, { credHelperSink: sink }));
94
96
  }
95
97
 
96
98
  case "switch": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-git-auth",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "pi coding-agent extension: git auth for GitHub and GitLab: keyring-stored login tokens, account switching, transparent git auth, repo list/create with details overlay",
5
5
  "license": "MIT",
6
6
  "author": "Carlo Onofrio",