create-op-node 0.10.0 → 0.10.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/cli.js +127 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -289,7 +289,7 @@ async function saveSecret(coords, value) {
|
|
|
289
289
|
return {
|
|
290
290
|
written: false,
|
|
291
291
|
updated,
|
|
292
|
-
reason: `security add-generic-password failed (${res.exitCode ?? "signal"}): ${res.stderr || res.stdout}`
|
|
292
|
+
reason: `security add-generic-password failed (${res.exitCode ?? "signal"}): ${res.stderr || res.stdout}${formatKeychainHint(res.exitCode)}`
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
295
|
return { written: true, updated };
|
|
@@ -309,6 +309,47 @@ async function readSecret(coords) {
|
|
|
309
309
|
const value = res.stdout.trim();
|
|
310
310
|
return value.length > 0 ? value : null;
|
|
311
311
|
}
|
|
312
|
+
var ERR_SEC_INTERACTION_NOT_ALLOWED = 36;
|
|
313
|
+
function formatKeychainHint(exitCode) {
|
|
314
|
+
if (exitCode === ERR_SEC_INTERACTION_NOT_ALLOWED) {
|
|
315
|
+
return `
|
|
316
|
+
Hint: the login keychain is locked (SSH sessions don't auto-unlock it).
|
|
317
|
+
Run \`security unlock-keychain ~/Library/Keychains/login.keychain-db\` and re-run bootstrap.`;
|
|
318
|
+
}
|
|
319
|
+
return "";
|
|
320
|
+
}
|
|
321
|
+
async function isKeychainLocked() {
|
|
322
|
+
const res = await safeExeca("security", [
|
|
323
|
+
"find-generic-password",
|
|
324
|
+
"-s",
|
|
325
|
+
`${SERVICE_PREFIX}.__op_node_lock_probe__`,
|
|
326
|
+
"-a",
|
|
327
|
+
"__nothing_here__"
|
|
328
|
+
]);
|
|
329
|
+
if (res === null) return false;
|
|
330
|
+
return res.exitCode === ERR_SEC_INTERACTION_NOT_ALLOWED;
|
|
331
|
+
}
|
|
332
|
+
async function unlockKeychain(password3) {
|
|
333
|
+
const res = await safeExeca("security", [
|
|
334
|
+
"unlock-keychain",
|
|
335
|
+
"-p",
|
|
336
|
+
password3,
|
|
337
|
+
// Default keychain path — same as the operator's GUI login keychain
|
|
338
|
+
// on a stock macOS install. Lets us avoid a homedir lookup here.
|
|
339
|
+
`${process.env["HOME"] ?? ""}/Library/Keychains/login.keychain-db`
|
|
340
|
+
]);
|
|
341
|
+
if (res === null) return { ok: false, reason: "`security` CLI not on PATH" };
|
|
342
|
+
if (res.exitCode !== 0) {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
reason: `security unlock-keychain failed (${res.exitCode ?? "signal"}): ${res.stderr || res.stdout}`
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return { ok: true };
|
|
349
|
+
}
|
|
350
|
+
function isSshSession(env = process.env) {
|
|
351
|
+
return Boolean(env["SSH_CONNECTION"] ?? env["SSH_TTY"]);
|
|
352
|
+
}
|
|
312
353
|
function unwrap(value) {
|
|
313
354
|
if (p3.isCancel(value)) {
|
|
314
355
|
p3.cancel("Cancelled.");
|
|
@@ -1452,20 +1493,72 @@ set -euo pipefail
|
|
|
1452
1493
|
|
|
1453
1494
|
SVC="org.opuspopuli.${input.region}"
|
|
1454
1495
|
|
|
1496
|
+
# macOS \`security\` exit codes:
|
|
1497
|
+
# 0 = success
|
|
1498
|
+
# 36 = errSecInteractionNotAllowed (keychain is locked)
|
|
1499
|
+
# 44 = errSecItemNotFound (entry doesn't exist)
|
|
1500
|
+
# Earlier wrapper versions conflated 36 and 44 into a generic "missing"
|
|
1501
|
+
# message, which sent operators down the wrong path (re-bootstrap when
|
|
1502
|
+
# the real fix was \`security unlock-keychain\`).
|
|
1455
1503
|
require_secret() {
|
|
1456
1504
|
local account="$1"
|
|
1457
1505
|
local value
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1506
|
+
local rc=0
|
|
1507
|
+
value=$(security find-generic-password -s "$SVC" -a "$account" -w 2>/dev/null) || rc=$?
|
|
1508
|
+
case "$rc" in
|
|
1509
|
+
0)
|
|
1510
|
+
printf '%s' "$value"
|
|
1511
|
+
return 0
|
|
1512
|
+
;;
|
|
1513
|
+
36)
|
|
1514
|
+
cat >&2 <<EOM
|
|
1515
|
+
op-compose: keychain is LOCKED \u2014 could not read entry '$account'.
|
|
1516
|
+
SSH sessions don't auto-unlock the login keychain. Unlock with:
|
|
1517
|
+
security unlock-keychain ~/Library/Keychains/login.keychain-db
|
|
1518
|
+
Then re-run this command.
|
|
1519
|
+
EOM
|
|
1520
|
+
exit 1
|
|
1521
|
+
;;
|
|
1522
|
+
44)
|
|
1523
|
+
cat >&2 <<EOM
|
|
1524
|
+
op-compose: keychain entry '$account' is MISSING under service '$SVC'.
|
|
1525
|
+
Run: create-op-node bootstrap --region ${input.region}
|
|
1526
|
+
EOM
|
|
1527
|
+
exit 1
|
|
1528
|
+
;;
|
|
1529
|
+
*)
|
|
1530
|
+
echo "op-compose: \`security\` failed reading '$account' (exit $rc)." >&2
|
|
1531
|
+
exit 1
|
|
1532
|
+
;;
|
|
1533
|
+
esac
|
|
1464
1534
|
}
|
|
1465
1535
|
|
|
1536
|
+
# Used for entries that may legitimately not exist (TUNNEL_TOKEN in
|
|
1537
|
+
# local-only mode, prompt-service secrets when colocation isn't active).
|
|
1538
|
+
# Treats exit 44 (missing) as a silent OK \u2014 returns empty string.
|
|
1539
|
+
# Treats exit 36 (locked) as a hard error, since the require_secret calls
|
|
1540
|
+
# at the top of the wrapper would have caught this first in normal use;
|
|
1541
|
+
# reaching this path means something else bypassed the lock check.
|
|
1466
1542
|
optional_secret() {
|
|
1467
1543
|
local account="$1"
|
|
1468
|
-
|
|
1544
|
+
local value
|
|
1545
|
+
local rc=0
|
|
1546
|
+
value=$(security find-generic-password -s "$SVC" -a "$account" -w 2>/dev/null) || rc=$?
|
|
1547
|
+
case "$rc" in
|
|
1548
|
+
0) printf '%s' "$value" ;;
|
|
1549
|
+
44) ;; # legitimately absent \u2014 empty string is correct
|
|
1550
|
+
36)
|
|
1551
|
+
cat >&2 <<EOM
|
|
1552
|
+
op-compose: keychain is LOCKED \u2014 could not probe optional entry '$account'.
|
|
1553
|
+
Unlock with: security unlock-keychain ~/Library/Keychains/login.keychain-db
|
|
1554
|
+
EOM
|
|
1555
|
+
exit 1
|
|
1556
|
+
;;
|
|
1557
|
+
*)
|
|
1558
|
+
echo "op-compose: \`security\` failed reading '$account' (exit $rc)." >&2
|
|
1559
|
+
exit 1
|
|
1560
|
+
;;
|
|
1561
|
+
esac
|
|
1469
1562
|
}
|
|
1470
1563
|
|
|
1471
1564
|
# Bootstrap-critical: Postgres + Supabase admin credentials.
|
|
@@ -2093,6 +2186,31 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2093
2186
|
);
|
|
2094
2187
|
process.exit(1);
|
|
2095
2188
|
}
|
|
2189
|
+
if (isSshSession() && await isKeychainLocked()) {
|
|
2190
|
+
p3.note(
|
|
2191
|
+
[
|
|
2192
|
+
"SSH session detected with a locked login keychain.",
|
|
2193
|
+
"macOS keychain operations require an unlocked keychain \u2014 your",
|
|
2194
|
+
"login password will be used once to unlock it for this session.",
|
|
2195
|
+
"The keychain remains unlocked until the session ends or it",
|
|
2196
|
+
"idle-locks. To skip and unlock manually, press Ctrl+C and run",
|
|
2197
|
+
"`security unlock-keychain` yourself."
|
|
2198
|
+
].join("\n"),
|
|
2199
|
+
"Keychain unlock required"
|
|
2200
|
+
);
|
|
2201
|
+
const pw = unwrap(
|
|
2202
|
+
await p3.password({
|
|
2203
|
+
message: "macOS login password (for `security unlock-keychain`):",
|
|
2204
|
+
validate: (v) => v && v.length > 0 ? void 0 : "password required"
|
|
2205
|
+
})
|
|
2206
|
+
);
|
|
2207
|
+
const unlock = await unlockKeychain(pw);
|
|
2208
|
+
if (!unlock.ok) {
|
|
2209
|
+
p3.cancel(unlock.reason ?? "security unlock-keychain failed.");
|
|
2210
|
+
process.exit(1);
|
|
2211
|
+
}
|
|
2212
|
+
p3.note(`${pc2.green("\u2713")} Keychain unlocked for this session.`, "Keychain");
|
|
2213
|
+
}
|
|
2096
2214
|
const pgsodiumKey = await loadSecret({
|
|
2097
2215
|
region,
|
|
2098
2216
|
account: "pgsodium-root-key",
|
|
@@ -4334,7 +4452,7 @@ async function looksLikeRegionsRepo(dir) {
|
|
|
4334
4452
|
}
|
|
4335
4453
|
|
|
4336
4454
|
// src/cli.ts
|
|
4337
|
-
var VERSION = "0.10.
|
|
4455
|
+
var VERSION = "0.10.2";
|
|
4338
4456
|
var program = new Command();
|
|
4339
4457
|
program.name("create-op-node").description(
|
|
4340
4458
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|