Skip to content

Commit 44244b4

Browse files
authored
Merge pull request #1 from githits-com/jlitola/win-keyring-chunking
feat: add chunked keyring storage for Windows
2 parents a76c9a2 + a3eb325 commit 44244b4

7 files changed

Lines changed: 697 additions & 6 deletions

File tree

bun.lock

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/implementation/auth.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ Each credential is a separate keychain entry using service name `"githits"`:
6565

6666
The `v1:` prefix allows future key format changes without collisions.
6767

68+
#### Windows chunked storage
69+
70+
Windows Credential Manager limits credential blobs to `CRED_MAX_CREDENTIAL_BLOB_SIZE` (2560 **bytes**). The `@napi-rs/keyring` binding encodes passwords as UTF-16 (2 bytes per character), so the effective character limit is 1280. Since JSON-serialized token data (especially JWT access tokens) can exceed this, the CLI wraps the `KeyringService` with a `ChunkingKeyringService` decorator on Windows (`process.platform === "win32"`). This decorator is not applied on macOS or Linux, which have no practical per-entry size limits.
71+
72+
When a value exceeds `WINDOWS_MAX_ENTRY_SIZE` (1200 characters — a conservative threshold providing 80-char margin from the 1280 limit), the decorator splits it across multiple keyring entries. The chunk size is configurable via the `ChunkingKeyringService` constructor, so the same decorator can be reused if other platforms have different limits:
73+
74+
| Account key pattern | Content |
75+
|---|---|
76+
| `<original-key>` | Sentinel: `CHUNKED:<writeId>:<count>` |
77+
| `<original-key>:chunk:<writeId>:0` | First chunk of the JSON value |
78+
| `<original-key>:chunk:<writeId>:N` | Nth chunk of the JSON value |
79+
80+
Each write uses a unique `writeId` to namespace chunk keys. This ensures atomicity: new chunks are written before the sentinel is updated, so a crash at any point leaves valid data. Old chunks are cleaned up after the sentinel is committed.
81+
82+
Values under 1200 characters are stored directly with no sentinel, maintaining full backward compatibility with pre-chunking CLI versions. If a user downgrades the CLI after tokens were stored as chunks, the old CLI reads the sentinel as raw text, fails JSON parsing (via `parseJsonOrNull`), and prompts re-login. The same applies to chunked client registrations, which would trigger re-registration. Both are acceptable graceful degradation.
83+
6884
The `getStorageLocation()` method returns a platform-specific label: "macOS Keychain (githits)" on macOS, "Windows Credential Manager (githits)" on Windows, and "System keychain (githits)" on Linux.
6985

7086
### File storage (fallback)
@@ -93,7 +109,9 @@ Keychain write must succeed before the file entry is deleted. Tokens and client
93109
```
94110
Container (createAuthStorage)
95111
└─ MigratingAuthStorage (decorator)
96-
├─ KeychainAuthStorage (primary) ← uses KeyringService
112+
├─ KeychainAuthStorage (primary)
113+
│ └─ ChunkingKeyringService (Windows only, decorator)
114+
│ └─ KeyringServiceImpl ← @napi-rs/keyring
97115
└─ AuthStorageImpl (legacy) ← file-based
98116
```
99117

@@ -129,6 +147,7 @@ The `hasValidToken` flag is checked by `requireAuth()` in `src/commands/mcp.ts`
129147
- **Token refresh fails silently** — By design. The container clears stale auth and `hasValidToken` becomes false, prompting re-login.
130148
- **Clearing auth** — Run `githits logout` to remove stored tokens and client registration for the current environment.
131149
- **Keychain unavailable warning** — If the system keychain is not accessible (headless Linux, CI), the CLI falls back to file storage in `~/.githits/` and prints a warning to stderr.
150+
- **Windows "password encoded as UTF-16 is longer than platform limit"** — The Windows Credential Manager limits credential blobs to 2560 bytes (`CRED_MAX_CREDENTIAL_BLOB_SIZE`). Since passwords are stored as UTF-16 (2 bytes per char), the effective limit is 1280 characters. The `ChunkingKeyringService` decorator handles this automatically by splitting large values across multiple entries. If this error occurs on an older CLI version, upgrade to get chunked storage support.
132151

133152
## Key Reference Files
134153

@@ -142,6 +161,7 @@ The `hasValidToken` flag is checked by `requireAuth()` in `src/commands/mcp.ts`
142161
| `src/services/auth-service.ts` | OAuth operations (DCR, PKCE, token exchange, callback server) |
143162
| `src/services/auth-storage.ts` | `AuthStorage` interface and file-based implementation |
144163
| `src/services/keyring-service.ts` | `KeyringService` interface wrapping `@napi-rs/keyring` |
164+
| `src/services/chunking-keyring-service.ts` | `KeyringService` decorator for chunked storage (Windows 2560-char limit) |
145165
| `src/services/keychain-auth-storage.ts` | `AuthStorage` implementation backed by system keychain |
146166
| `src/services/migrating-auth-storage.ts` | Migration decorator (keychain primary + file legacy) |
147167
| `src/services/filesystem-service.ts` | File system abstraction for testable storage |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "githits",
33
"description": "CLI companion for GitHits - code examples from global open source for developers and AI assistants",
4-
"version": "0.1.0",
4+
"version": "0.1.1",
55
"type": "module",
66
"files": [
77
"dist",

src/container.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
AuthStorageImpl,
77
type BrowserService,
88
BrowserServiceImpl,
9+
ChunkingKeyringService,
910
type FileSystemService,
1011
FileSystemServiceImpl,
1112
GitHitsServiceImpl,
@@ -18,6 +19,7 @@ import {
1819
MigratingAuthStorage,
1920
RefreshingGitHitsService,
2021
TokenManager,
22+
WINDOWS_MAX_ENTRY_SIZE,
2123
} from "./services/index.js";
2224

2325
/**
@@ -29,9 +31,16 @@ function createAuthStorage(fileSystemService: FileSystemService): AuthStorage {
2931
const fileStorage = new AuthStorageImpl(fileSystemService);
3032

3133
try {
32-
const keyring = new KeyringServiceImpl();
34+
const rawKeyring = new KeyringServiceImpl();
35+
// Windows Credential Manager limits entries to 2560 UTF-16 chars.
36+
// Wrap with chunking decorator to split large values across multiple entries.
37+
const keyring =
38+
process.platform === "win32"
39+
? new ChunkingKeyringService(rawKeyring, WINDOWS_MAX_ENTRY_SIZE)
40+
: rawKeyring;
3341
// Probe keychain availability with a write+delete cycle.
3442
// Use timestamp + random suffix to avoid probe key collisions.
43+
// Probe value "probe" is 5 chars, passes through the chunking wrapper unchanged.
3544
const probeKey = `__probe_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3645
keyring.setPassword("githits", probeKey, "probe");
3746
try {

0 commit comments

Comments
 (0)