-
Notifications
You must be signed in to change notification settings - Fork 0
keychainsy #1280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
keychainsy #1280
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,12 @@ let | |
| ) | ||
| ); | ||
|
|
||
| keychainSyncScript = pkgs.replaceVars ./scripts/keychain-sync.sh { | ||
| email = "shunkakinoki@gmail.com"; | ||
| keychain_account = "shunkakinoki"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| jq = "${pkgs.jq}/bin/jq"; | ||
| }; | ||
|
|
||
| wrapperScript = pkgs.replaceVars ./scripts/wrapper.sh { | ||
| common = commonScript; | ||
| }; | ||
|
|
@@ -105,6 +111,53 @@ in | |
| }; | ||
| }; | ||
|
|
||
| # Keychain sync - extract Claude/Codex OAuth from local stores into auth dir | ||
| launchd.agents.cliproxyapi-keychain-sync = lib.mkIf pkgs.stdenv.isDarwin { | ||
| enable = true; | ||
| config = { | ||
| ProgramArguments = [ | ||
| "${pkgs.bash}/bin/bash" | ||
| "${keychainSyncScript}" | ||
| ]; | ||
| Environment = { | ||
| PATH = "${ | ||
| lib.makeBinPath [ | ||
| pkgs.bash | ||
| pkgs.coreutils | ||
| pkgs.jq | ||
| ] | ||
| }:/usr/bin"; | ||
| }; | ||
| StartInterval = 300; | ||
| RunAtLoad = true; | ||
| StandardOutPath = "/tmp/cliproxyapi-keychain-sync.log"; | ||
| StandardErrorPath = "/tmp/cliproxyapi-keychain-sync.error.log"; | ||
|
Comment on lines
+133
to
+134
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logging to |
||
| }; | ||
| }; | ||
|
|
||
| # Periodic sync - pull auth files from S3 every 5 minutes | ||
| launchd.agents.cliproxyapi-sync = lib.mkIf pkgs.stdenv.isDarwin { | ||
| enable = true; | ||
| config = { | ||
| ProgramArguments = [ | ||
| "${pkgs.bash}/bin/bash" | ||
| "${hydrateScript}" | ||
| ]; | ||
| Environment = { | ||
| PATH = "${ | ||
| lib.makeBinPath [ | ||
| pkgs.bash | ||
| pkgs.coreutils | ||
| pkgs.awscli2 | ||
| ] | ||
| }:/opt/homebrew/bin:/usr/local/bin:/usr/bin"; | ||
| }; | ||
| StartInterval = 300; | ||
| StandardOutPath = "/tmp/cliproxyapi-sync.log"; | ||
| StandardErrorPath = "/tmp/cliproxyapi-sync.error.log"; | ||
|
Comment on lines
+156
to
+157
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }; | ||
| }; | ||
|
|
||
| # Linux systemd | ||
| systemd.user.services.cliproxyapi = lib.mkIf pkgs.stdenv.isLinux { | ||
| Unit = { | ||
|
|
@@ -160,4 +213,30 @@ in | |
| }"; | ||
| }; | ||
| }; | ||
|
|
||
| # Periodic sync - pull auth files from S3 every 5 minutes | ||
| systemd.user.timers.cliproxyapi-sync = lib.mkIf pkgs.stdenv.isLinux { | ||
| Unit.Description = "Periodically sync auth files from S3"; | ||
| Timer = { | ||
| OnBootSec = "1min"; | ||
| OnUnitActiveSec = "5min"; | ||
| Unit = "cliproxyapi-sync.service"; | ||
| }; | ||
| Install.WantedBy = [ "timers.target" ]; | ||
| }; | ||
|
|
||
| systemd.user.services.cliproxyapi-sync = lib.mkIf pkgs.stdenv.isLinux { | ||
| Unit.Description = "CLIProxyAPI auth sync from S3"; | ||
| Service = { | ||
| Type = "oneshot"; | ||
| ExecStart = "${pkgs.bash}/bin/bash ${hydrateScript}"; | ||
| Environment = "PATH=${ | ||
| lib.makeBinPath [ | ||
| pkgs.bash | ||
| pkgs.awscli2 | ||
| pkgs.coreutils | ||
| ] | ||
| }"; | ||
| }; | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,143 @@ | ||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||
| # Sync OAuth tokens from local credential stores into cliproxyapi auth dir. | ||||||||||||||||
| # | ||||||||||||||||
| # Sources: | ||||||||||||||||
| # - Claude Code: macOS Keychain (Claude Code-credentials / <username>) | ||||||||||||||||
| # - Codex CLI: ~/.codex/auth.json | ||||||||||||||||
| # | ||||||||||||||||
| # On first run, macOS will prompt to allow keychain access — click "Always Allow". | ||||||||||||||||
| # shellcheck source=/dev/null | ||||||||||||||||
| set -euo pipefail | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The script uses References
|
||||||||||||||||
|
|
||||||||||||||||
| AUTH_DIR="${HOME}/.cli-proxy-api/objectstore/auths" | ||||||||||||||||
| EMAIL="@email@" | ||||||||||||||||
| KEYCHAIN_ACCOUNT="@keychain_account@" | ||||||||||||||||
| JQ="@jq@" | ||||||||||||||||
|
|
||||||||||||||||
| SECURITY="${SECURITY:-/usr/bin/security}" | ||||||||||||||||
|
|
||||||||||||||||
| mkdir -p "$AUTH_DIR" | ||||||||||||||||
|
|
||||||||||||||||
| changed=0 | ||||||||||||||||
|
|
||||||||||||||||
| # --- Claude Code (keychain) --- | ||||||||||||||||
| sync_claude() { | ||||||||||||||||
| local raw | ||||||||||||||||
| raw=$("$SECURITY" find-generic-password \ | ||||||||||||||||
| -s "Claude Code-credentials" \ | ||||||||||||||||
| -a "$KEYCHAIN_ACCOUNT" \ | ||||||||||||||||
| -w 2>/dev/null) || return 0 | ||||||||||||||||
|
|
||||||||||||||||
| if [ -z "$raw" ]; then | ||||||||||||||||
| echo "[$(date)] Claude: keychain entry empty, skipping" >&2 | ||||||||||||||||
| return 0 | ||||||||||||||||
| fi | ||||||||||||||||
|
|
||||||||||||||||
| # Claude Code stores JSON: { claudeAiOauth: { accessToken, refreshToken, expiresAt (epoch ms), ... } } | ||||||||||||||||
| local access_token refresh_token expires_at | ||||||||||||||||
| access_token=$($JQ -r '.claudeAiOauth.accessToken // empty' <<<"$raw" 2>/dev/null) || true | ||||||||||||||||
| refresh_token=$($JQ -r '.claudeAiOauth.refreshToken // empty' <<<"$raw" 2>/dev/null) || true | ||||||||||||||||
| expires_at=$($JQ -r '.claudeAiOauth.expiresAt // empty' <<<"$raw" 2>/dev/null) || true | ||||||||||||||||
|
|
||||||||||||||||
| # Convert epoch ms to ISO 8601 | ||||||||||||||||
| if [ -n "$expires_at" ] && [ "$expires_at" != "null" ]; then | ||||||||||||||||
| expires_at=$(date -u -r "$((expires_at / 1000))" +%Y-%m-%dT%H:%M:%S+00:00 2>/dev/null) || expires_at="" | ||||||||||||||||
|
||||||||||||||||
| expires_at=$(date -u -r "$((expires_at / 1000))" +%Y-%m-%dT%H:%M:%S+00:00 2>/dev/null) || expires_at="" | |
| expires_at=$(/bin/date -u -r "$((expires_at / 1000))" +%Y-%m-%dT%H:%M:%S+00:00 2>/dev/null) || expires_at="" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing directly to $dest using printf '%s' "$new_json" >"$dest" is not atomic. If the script is interrupted during the write operation, the destination file could be corrupted or left in an incomplete state. For sensitive data like authentication tokens, it's safer to write to a temporary file first and then atomically move it to the final destination.
| if [ "$access_token" != "$existing_at" ]; then | |
| printf '%s' "$new_json" >"${dest}.tmp" && mv "${dest}.tmp" "$dest" |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Auth JSON files containing access tokens are written with the process umask and via direct redirection. On many systems this can create world-readable files (e.g. mode 0644) and it’s also non-atomic (a watcher could read a partially-written file). Consider setting a restrictive umask / chmod to 0600 and writing via a temp file + atomic rename (mv) to avoid token exposure and partial reads.
| printf '%s' "$new_json" >"$dest" | |
| # Write via a temp file with restrictive permissions, then atomically rename | |
| local tmp_dest | |
| tmp_dest=$(mktemp "${dest}.tmp.XXXXXX") | |
| printf '%s' "$new_json" >"$tmp_dest" | |
| chmod 600 "$tmp_dest" | |
| mv -f "$tmp_dest" "$dest" |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Codex account_id is extracted from .account_id, but the Codex auth format used elsewhere (and in this repo’s spec fixtures) nests it under .tokens.account_id. This will always write an empty account_id in the generated cliproxyapi auth JSON. Update the jq path to read the correct field (optionally supporting both layouts for compatibility).
| account_id=$($JQ -r '.account_id // empty' "$auth_file" 2>/dev/null) || true | |
| account_id=$($JQ -r '.tokens.account_id // .account_id // empty' "$auth_file" 2>/dev/null) || true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the Claude sync function, this write operation is not atomic. Consider using a temporary file and then atomically moving it to prevent data corruption in case of interruption.
| if [ "$access_token" != "$existing_at" ]; then | |
| printf '%s' "$new_json" >"${dest}.tmp" && mv "${dest}.tmp" "$dest" |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above for the Codex auth file: writing secrets via direct redirection can create overly-permissive permissions and is not atomic, which can race with the backup watcher reading/syncing. Prefer restrictive permissions (0600) and an atomic write pattern (temp file + rename).
| printf '%s' "$new_json" >"$dest" | |
| umask 077 | |
| local tmp | |
| tmp="$(mktemp "${dest}.tmp.XXXXXX")" | |
| printf '%s' "$new_json" >"$tmp" | |
| chmod 600 "$tmp" 2>/dev/null || true | |
| mv "$tmp" "$dest" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
emailforkeychainSyncScriptis hardcoded. It would be more flexible and maintainable to make this configurable, perhaps by deriving it fromconfig.home.usernameor allowing it to be passed as an argument, especially if this configuration is intended for multiple users or environments.