even-keel 0.2.0-rc.1 → 0.2.0-rc.2
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/dist/src/account/keychain.js +44 -13
- package/package.json +1 -1
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
const SERVICE = 'Claude Code-credentials';
|
|
4
|
-
/** The `-a` the CLI writes its sign-in item with (CLI 2.1.317). */
|
|
5
|
-
const ACCOUNT = 'claude-code-user';
|
|
6
4
|
const NOT_FOUND = 44;
|
|
7
5
|
const TTL_MS = 60_000;
|
|
8
6
|
const TIMEOUT_MS = 3_000;
|
|
@@ -53,32 +51,53 @@ export class KeychainCredentials {
|
|
|
53
51
|
}
|
|
54
52
|
/**
|
|
55
53
|
* Carries a folder's sign-in item to the name of the folder's new path (BTN-745): the `.pending-*` folder of "Add
|
|
56
|
-
* account" is renamed after the sign-in, and its item would stay behind matching no folder.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
54
|
+
* account" is renamed after the sign-in, and its item would stay behind matching no folder. The old item is found by
|
|
55
|
+
* its name alone and keeps its own account (`acct`): the CLI writes the macOS user name there (BTN-748). An item
|
|
56
|
+
* already under the new name, whatever its account, wins and the old one is only deleted; a failed write keeps the old
|
|
57
|
+
* one. The secret goes to `security -i` on stdin as hex, as the CLI writes it, never in the arguments.
|
|
59
58
|
*/
|
|
60
59
|
move(from, to) {
|
|
61
60
|
this.forget();
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
61
|
+
const found = this.ask(['find-generic-password', '-s', from]);
|
|
62
|
+
if (found === undefined)
|
|
64
63
|
return;
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
if (found.status !== 0) {
|
|
65
|
+
this.warn(found.status === NOT_FOUND
|
|
66
|
+
? `Keychain: no item "${from}" to carry to "${to}"; the sign-in may be in .credentials.json`
|
|
67
|
+
: `Keychain: could not look up "${from}" (security exit ${found.status}); nothing carried to "${to}"`);
|
|
67
68
|
return;
|
|
68
|
-
|
|
69
|
+
}
|
|
70
|
+
const account = accountOf(found.stdout);
|
|
71
|
+
if (account !== null && !QUOTABLE.test(account)) {
|
|
72
|
+
this.warn(`Keychain: the account of "${from}" cannot be passed to security -i; the sign-in stays there`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const by = account === null ? [] : ['-a', account];
|
|
76
|
+
const old = this.ask(['find-generic-password', ...by, '-s', from, '-w']);
|
|
77
|
+
if (old === undefined)
|
|
78
|
+
return;
|
|
79
|
+
const text = old.status === 0 ? decode(old.stdout.trim()) : undefined;
|
|
80
|
+
if (text === undefined) {
|
|
81
|
+
this.warn(`Keychain: could not read "${from}"; nothing carried to "${to}"`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const present = this.ask(['find-generic-password', '-s', to]);
|
|
69
85
|
if (present === undefined)
|
|
70
86
|
return;
|
|
71
87
|
if (present.status === NOT_FOUND) {
|
|
72
88
|
const hex = Buffer.from(text, 'utf8').toString('hex');
|
|
73
|
-
const
|
|
89
|
+
const as = account === null ? '' : ` -a "${account}"`;
|
|
90
|
+
const written = this.ask(['-i'], `add-generic-password -U${as} -s "${to}" -X "${hex}"\n`);
|
|
74
91
|
if (written?.status !== 0) {
|
|
75
92
|
this.warn(`Keychain: could not write "${to}"; the sign-in stays under "${from}"`);
|
|
76
93
|
return;
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
|
-
else if (present.status !== 0)
|
|
96
|
+
else if (present.status !== 0) {
|
|
97
|
+
this.warn(`Keychain: could not look up "${to}" (security exit ${present.status}); the sign-in stays under "${from}"`);
|
|
80
98
|
return;
|
|
81
|
-
|
|
99
|
+
}
|
|
100
|
+
if (this.ask(['delete-generic-password', ...by, '-s', from])?.status !== 0)
|
|
82
101
|
this.warn(`Keychain: could not delete "${from}"`);
|
|
83
102
|
}
|
|
84
103
|
/** One `security` call; `undefined` when it timed out, likely on an access dialog. */
|
|
@@ -93,6 +112,18 @@ export class KeychainCredentials {
|
|
|
93
112
|
this.answers.clear();
|
|
94
113
|
}
|
|
95
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The `acct` attribute from the dump of `find-generic-password` without `-w`: `"acct"<blob>="m1"`, or `0x<hex>` and an
|
|
117
|
+
* escaped copy when a byte is outside printable ASCII; `null` for `<NULL>` or no such line.
|
|
118
|
+
*/
|
|
119
|
+
function accountOf(dump) {
|
|
120
|
+
const line = /^\s*"acct"<blob>=(?:"(.*)"|0x([0-9a-f]*))/im.exec(dump);
|
|
121
|
+
if (line === null)
|
|
122
|
+
return null;
|
|
123
|
+
return line[1] ?? Buffer.from(line[2], 'hex').toString('utf8');
|
|
124
|
+
}
|
|
125
|
+
/** `security -i` splits its line on quotes: an account with a quote, a backslash or a control character is not passed. */
|
|
126
|
+
const QUOTABLE = /^[^"\\\x00-\x1f\x7f]*$/;
|
|
96
127
|
/** `security -w` prints a secret that is not plain text as hex. */
|
|
97
128
|
function decode(text) {
|
|
98
129
|
if (!text)
|
package/package.json
CHANGED