browser-automation-skill 0.71.0
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/LICENSE +21 -0
- package/README.md +144 -0
- package/SECURITY.md +39 -0
- package/SKILL.md +206 -0
- package/bin/cli.mjs +55 -0
- package/install.sh +143 -0
- package/package.json +54 -0
- package/references/adapter-candidates.md +40 -0
- package/references/browser-mcp-cheatsheet.md +132 -0
- package/references/browser-stats-cheatsheet.md +155 -0
- package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
- package/references/midscene-integration.md +359 -0
- package/references/obscura-cheatsheet.md +103 -0
- package/references/playwright-cli-cheatsheet.md +64 -0
- package/references/playwright-lib-cheatsheet.md +90 -0
- package/references/recipes/add-a-tool-adapter.md +134 -0
- package/references/recipes/agent-workflows/README.md +37 -0
- package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
- package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
- package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
- package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
- package/references/recipes/anti-patterns-tool-extension.md +182 -0
- package/references/recipes/body-bytes-not-body.md +139 -0
- package/references/recipes/cache-write-security.md +210 -0
- package/references/recipes/fingerprint-rescue.md +154 -0
- package/references/recipes/model-routing.md +143 -0
- package/references/recipes/path-security.md +138 -0
- package/references/recipes/privacy-canary.md +96 -0
- package/references/recipes/visual-rescue-hook.md +182 -0
- package/references/stats-prices.json +42 -0
- package/references/stats-schema.json +77 -0
- package/references/tool-versions.md +8 -0
- package/scripts/browser-add-site.sh +113 -0
- package/scripts/browser-assert.sh +106 -0
- package/scripts/browser-audit.sh +68 -0
- package/scripts/browser-baseline.sh +135 -0
- package/scripts/browser-click.sh +100 -0
- package/scripts/browser-creds-add.sh +254 -0
- package/scripts/browser-creds-list.sh +67 -0
- package/scripts/browser-creds-migrate.sh +122 -0
- package/scripts/browser-creds-remove.sh +69 -0
- package/scripts/browser-creds-rotate-totp.sh +109 -0
- package/scripts/browser-creds-show.sh +82 -0
- package/scripts/browser-creds-totp.sh +94 -0
- package/scripts/browser-do.sh +630 -0
- package/scripts/browser-doctor.sh +365 -0
- package/scripts/browser-drag.sh +90 -0
- package/scripts/browser-extract.sh +192 -0
- package/scripts/browser-fill.sh +142 -0
- package/scripts/browser-flow.sh +316 -0
- package/scripts/browser-history.sh +187 -0
- package/scripts/browser-hover.sh +92 -0
- package/scripts/browser-inspect.sh +188 -0
- package/scripts/browser-list-sessions.sh +78 -0
- package/scripts/browser-list-sites.sh +42 -0
- package/scripts/browser-login.sh +279 -0
- package/scripts/browser-mcp.sh +65 -0
- package/scripts/browser-migrate.sh +195 -0
- package/scripts/browser-open.sh +134 -0
- package/scripts/browser-press.sh +80 -0
- package/scripts/browser-remove-session.sh +72 -0
- package/scripts/browser-remove-site.sh +68 -0
- package/scripts/browser-replay.sh +206 -0
- package/scripts/browser-route.sh +174 -0
- package/scripts/browser-select.sh +122 -0
- package/scripts/browser-show-session.sh +57 -0
- package/scripts/browser-show-site.sh +37 -0
- package/scripts/browser-snapshot.sh +176 -0
- package/scripts/browser-stats.sh +522 -0
- package/scripts/browser-tab-close.sh +112 -0
- package/scripts/browser-tab-list.sh +70 -0
- package/scripts/browser-tab-switch.sh +111 -0
- package/scripts/browser-upload.sh +132 -0
- package/scripts/browser-use.sh +60 -0
- package/scripts/browser-vlm.sh +707 -0
- package/scripts/browser-wait.sh +97 -0
- package/scripts/install-git-hooks.sh +16 -0
- package/scripts/lib/capture.sh +356 -0
- package/scripts/lib/common.sh +262 -0
- package/scripts/lib/credential.sh +237 -0
- package/scripts/lib/fingerprint-rescue.js +123 -0
- package/scripts/lib/flow.sh +448 -0
- package/scripts/lib/flow_record.sh +210 -0
- package/scripts/lib/mask.sh +49 -0
- package/scripts/lib/memory.sh +427 -0
- package/scripts/lib/migrate.sh +390 -0
- package/scripts/lib/migrators/README.md +23 -0
- package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
- package/scripts/lib/migrators/recent_urls/README.md +13 -0
- package/scripts/lib/migrators/stats/README.md +24 -0
- package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
- package/scripts/lib/node/mcp-server.mjs +531 -0
- package/scripts/lib/node/mcp-tools.json +68 -0
- package/scripts/lib/node/playwright-driver.mjs +1104 -0
- package/scripts/lib/node/totp-core.mjs +52 -0
- package/scripts/lib/node/totp.mjs +52 -0
- package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
- package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
- package/scripts/lib/output.sh +79 -0
- package/scripts/lib/router.sh +342 -0
- package/scripts/lib/sanitize.sh +107 -0
- package/scripts/lib/secret/keychain.sh +91 -0
- package/scripts/lib/secret/libsecret.sh +74 -0
- package/scripts/lib/secret/plaintext.sh +75 -0
- package/scripts/lib/secret_backend_select.sh +57 -0
- package/scripts/lib/session.sh +153 -0
- package/scripts/lib/site.sh +126 -0
- package/scripts/lib/stats.sh +419 -0
- package/scripts/lib/tool/.gitkeep +0 -0
- package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
- package/scripts/lib/tool/obscura.sh +249 -0
- package/scripts/lib/tool/playwright-cli.sh +155 -0
- package/scripts/lib/tool/playwright-lib.sh +106 -0
- package/scripts/lib/verb_helpers.sh +222 -0
- package/scripts/lib/visual-rescue-default.sh +145 -0
- package/scripts/regenerate-docs.sh +99 -0
- package/uninstall.sh +51 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-baseline.sh — named-blessed-capture management. Phase 9
|
|
3
|
+
# part 1-v (CLOSES Phase 9).
|
|
4
|
+
#
|
|
5
|
+
# Sub-modes:
|
|
6
|
+
# save <capture-id> --as NAME — set is_baseline:true; write baselines.json entry
|
|
7
|
+
# list — emit one baseline_row per entry
|
|
8
|
+
# remove <NAME> — clear is_baseline; splice baselines.json
|
|
9
|
+
# (does NOT delete the capture dir; use
|
|
10
|
+
# history clear for that)
|
|
11
|
+
#
|
|
12
|
+
# Per locked decision B1: thin wrapper over Phase 7's meta.is_baseline:true.
|
|
13
|
+
# capture_prune already honors the skip-rule (landed in 7-1-v as forward-compat
|
|
14
|
+
# for Phase 9). NO new prune logic here.
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
IFS=$'\n\t'
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
SCRIPTS_DIR="${SCRIPT_DIR}"
|
|
21
|
+
export SCRIPTS_DIR
|
|
22
|
+
|
|
23
|
+
# shellcheck source=lib/common.sh
|
|
24
|
+
# shellcheck disable=SC1091
|
|
25
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
26
|
+
# shellcheck source=lib/output.sh
|
|
27
|
+
# shellcheck disable=SC1091
|
|
28
|
+
source "${SCRIPT_DIR}/lib/output.sh"
|
|
29
|
+
|
|
30
|
+
init_paths
|
|
31
|
+
|
|
32
|
+
SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
|
|
33
|
+
|
|
34
|
+
readonly BASELINES_FILE="${BROWSER_SKILL_HOME}/baselines.json"
|
|
35
|
+
|
|
36
|
+
# Lazy-create baselines.json on first save (mode 0600).
|
|
37
|
+
_baseline_init_file() {
|
|
38
|
+
if [ ! -f "${BASELINES_FILE}" ]; then
|
|
39
|
+
jq -nc '{schema_version: 1, baselines: []}' > "${BASELINES_FILE}"
|
|
40
|
+
chmod 600 "${BASELINES_FILE}"
|
|
41
|
+
fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
sub_mode="${1:-}"
|
|
45
|
+
[ -n "${sub_mode}" ] || die "${EXIT_USAGE_ERROR}" "browser-baseline: missing sub-mode (save / list / remove)"
|
|
46
|
+
shift
|
|
47
|
+
|
|
48
|
+
case "${sub_mode}" in
|
|
49
|
+
save)
|
|
50
|
+
capture_id="${1:-}"
|
|
51
|
+
[ -n "${capture_id}" ] || die "${EXIT_USAGE_ERROR}" "baseline save: missing <capture-id>"
|
|
52
|
+
shift
|
|
53
|
+
name=""
|
|
54
|
+
while [ "$#" -gt 0 ]; do
|
|
55
|
+
case "$1" in
|
|
56
|
+
--as) name="$2"; shift 2 ;;
|
|
57
|
+
*) die "${EXIT_USAGE_ERROR}" "baseline save: unknown flag '$1'" ;;
|
|
58
|
+
esac
|
|
59
|
+
done
|
|
60
|
+
[ -n "${name}" ] || die "${EXIT_USAGE_ERROR}" "baseline save: --as NAME is required"
|
|
61
|
+
|
|
62
|
+
meta="${CAPTURES_DIR}/${capture_id}/meta.json"
|
|
63
|
+
[ -f "${meta}" ] || die "${EXIT_USAGE_ERROR}" "baseline save: no such capture '${capture_id}'"
|
|
64
|
+
|
|
65
|
+
# Set is_baseline:true on meta.json (Phase 7's prune skip-rule honors this).
|
|
66
|
+
tmp="${meta}.tmp.$$"
|
|
67
|
+
jq '.is_baseline = true' "${meta}" > "${tmp}"
|
|
68
|
+
chmod 600 "${tmp}"
|
|
69
|
+
mv "${tmp}" "${meta}"
|
|
70
|
+
|
|
71
|
+
# Append entry to baselines.json.
|
|
72
|
+
_baseline_init_file
|
|
73
|
+
saved_at="$(now_iso)"
|
|
74
|
+
summary_obj="$(jq -c '{verb, flow_name, step_count}' "${meta}")"
|
|
75
|
+
tmp="${BASELINES_FILE}.tmp.$$"
|
|
76
|
+
jq -c \
|
|
77
|
+
--arg name "${name}" \
|
|
78
|
+
--arg cid "${capture_id}" \
|
|
79
|
+
--arg ts "${saved_at}" \
|
|
80
|
+
--argjson sum "${summary_obj}" \
|
|
81
|
+
'.baselines += [{name: $name, capture_id: $cid, saved_at: $ts, summary: $sum}]' \
|
|
82
|
+
"${BASELINES_FILE}" > "${tmp}"
|
|
83
|
+
chmod 600 "${tmp}"
|
|
84
|
+
mv "${tmp}" "${BASELINES_FILE}"
|
|
85
|
+
|
|
86
|
+
emit_summary verb=baseline tool=none why=save status=ok mode=save \
|
|
87
|
+
capture_id="${capture_id}" name="${name}"
|
|
88
|
+
exit 0
|
|
89
|
+
;;
|
|
90
|
+
|
|
91
|
+
list)
|
|
92
|
+
_baseline_init_file
|
|
93
|
+
total=0
|
|
94
|
+
while IFS= read -r row; do
|
|
95
|
+
[ -z "${row}" ] && continue
|
|
96
|
+
printf '%s' "${row}" | jq -c '. + {event: "baseline_row"}'
|
|
97
|
+
total=$((total + 1))
|
|
98
|
+
done <<< "$(jq -c '.baselines[]?' "${BASELINES_FILE}")"
|
|
99
|
+
emit_summary verb=baseline tool=none why=list status=ok mode=list total="${total}"
|
|
100
|
+
exit 0
|
|
101
|
+
;;
|
|
102
|
+
|
|
103
|
+
remove)
|
|
104
|
+
name="${1:-}"
|
|
105
|
+
[ -n "${name}" ] || die "${EXIT_USAGE_ERROR}" "baseline remove: missing <name>"
|
|
106
|
+
_baseline_init_file
|
|
107
|
+
# Find the capture-id for the given name.
|
|
108
|
+
capture_id="$(jq -r --arg n "${name}" '.baselines[] | select(.name == $n) | .capture_id' "${BASELINES_FILE}")"
|
|
109
|
+
[ -n "${capture_id}" ] || die "${EXIT_USAGE_ERROR}" "baseline remove: no such baseline '${name}'"
|
|
110
|
+
|
|
111
|
+
# Clear is_baseline on meta.json (if capture still exists; fail-soft).
|
|
112
|
+
meta="${CAPTURES_DIR}/${capture_id}/meta.json"
|
|
113
|
+
if [ -f "${meta}" ]; then
|
|
114
|
+
tmp="${meta}.tmp.$$"
|
|
115
|
+
jq '.is_baseline = false' "${meta}" > "${tmp}"
|
|
116
|
+
chmod 600 "${tmp}"
|
|
117
|
+
mv "${tmp}" "${meta}"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Splice baselines.json.
|
|
121
|
+
tmp="${BASELINES_FILE}.tmp.$$"
|
|
122
|
+
jq -c --arg n "${name}" '.baselines |= map(select(.name != $n))' \
|
|
123
|
+
"${BASELINES_FILE}" > "${tmp}"
|
|
124
|
+
chmod 600 "${tmp}"
|
|
125
|
+
mv "${tmp}" "${BASELINES_FILE}"
|
|
126
|
+
|
|
127
|
+
emit_summary verb=baseline tool=none why=remove status=ok mode=remove \
|
|
128
|
+
capture_id="${capture_id}" name="${name}"
|
|
129
|
+
exit 0
|
|
130
|
+
;;
|
|
131
|
+
|
|
132
|
+
*)
|
|
133
|
+
die "${EXIT_USAGE_ERROR}" "browser-baseline: unknown sub-mode '${sub_mode}' (use save / list / remove)"
|
|
134
|
+
;;
|
|
135
|
+
esac
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-click.sh — click an element by --ref eN or --selector CSS.
|
|
3
|
+
# Usage: bash scripts/browser-click.sh [--site NAME] [--tool NAME] [--dry-run]
|
|
4
|
+
# [--raw] (--ref eN | --selector CSS)
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
IFS=$'\n\t'
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
# shellcheck source=lib/common.sh
|
|
11
|
+
# shellcheck disable=SC1091
|
|
12
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
13
|
+
# shellcheck source=lib/output.sh
|
|
14
|
+
# shellcheck disable=SC1091
|
|
15
|
+
source "${SCRIPT_DIR}/lib/output.sh"
|
|
16
|
+
# shellcheck source=lib/router.sh
|
|
17
|
+
# shellcheck disable=SC1091
|
|
18
|
+
source "${SCRIPT_DIR}/lib/router.sh"
|
|
19
|
+
# shellcheck source=lib/verb_helpers.sh
|
|
20
|
+
# shellcheck disable=SC1091
|
|
21
|
+
source "${SCRIPT_DIR}/lib/verb_helpers.sh"
|
|
22
|
+
# shellcheck source=lib/stats.sh
|
|
23
|
+
# shellcheck disable=SC1091
|
|
24
|
+
source "${SCRIPT_DIR}/lib/stats.sh"
|
|
25
|
+
|
|
26
|
+
init_paths
|
|
27
|
+
|
|
28
|
+
SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
|
|
29
|
+
|
|
30
|
+
parse_verb_globals "$@"
|
|
31
|
+
|
|
32
|
+
# Resolve site/session → BROWSER_SKILL_STORAGE_STATE (no-op if neither set).
|
|
33
|
+
# Router's rule_session_required reads the env var to prefer playwright-lib.
|
|
34
|
+
resolve_session_storage_state
|
|
35
|
+
|
|
36
|
+
ref="" selector=""
|
|
37
|
+
verb_argv=()
|
|
38
|
+
i=0
|
|
39
|
+
while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
|
|
40
|
+
case "${REMAINING_ARGV[i]}" in
|
|
41
|
+
--ref)
|
|
42
|
+
ref="${REMAINING_ARGV[i+1]:-}"
|
|
43
|
+
[ -n "${ref}" ] || die "${EXIT_USAGE_ERROR}" "--ref requires a value"
|
|
44
|
+
verb_argv+=(--ref "${ref}")
|
|
45
|
+
i=$((i + 2))
|
|
46
|
+
;;
|
|
47
|
+
--selector)
|
|
48
|
+
selector="${REMAINING_ARGV[i+1]:-}"
|
|
49
|
+
[ -n "${selector}" ] || die "${EXIT_USAGE_ERROR}" "--selector requires a value"
|
|
50
|
+
verb_argv+=(--selector "${selector}")
|
|
51
|
+
i=$((i + 2))
|
|
52
|
+
;;
|
|
53
|
+
*)
|
|
54
|
+
verb_argv+=("${REMAINING_ARGV[i]}")
|
|
55
|
+
i=$((i + 1))
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
if [ -n "${ref}" ] && [ -n "${selector}" ]; then
|
|
61
|
+
die "${EXIT_USAGE_ERROR}" "--ref and --selector are mutually exclusive"
|
|
62
|
+
fi
|
|
63
|
+
if [ -z "${ref}" ] && [ -z "${selector}" ]; then
|
|
64
|
+
die "${EXIT_USAGE_ERROR}" "click requires --ref eN or --selector CSS"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
|
|
68
|
+
ok "dry-run: would click ${ref:-${selector}}"
|
|
69
|
+
emit_summary verb=click tool=none why=dry-run status=ok ref="${ref}" selector="${selector}" dry_run=true
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
picked="$(pick_tool click "${verb_argv[@]}")"
|
|
74
|
+
tool_name="${picked%%$'\t'*}"
|
|
75
|
+
why="${picked#*$'\t'}"
|
|
76
|
+
|
|
77
|
+
source_picked_adapter "${tool_name}"
|
|
78
|
+
|
|
79
|
+
stats_t0="$(now_ms)"
|
|
80
|
+
set +e
|
|
81
|
+
adapter_out="$(invoke_with_retry click "${verb_argv[@]}")"
|
|
82
|
+
adapter_rc=$?
|
|
83
|
+
set -e
|
|
84
|
+
|
|
85
|
+
# Phase 12 part 1: per-action telemetry. No natural observed value for click;
|
|
86
|
+
# observed=adapter_out so element_value post-conditions can match against any
|
|
87
|
+
# response payload the adapter emits.
|
|
88
|
+
BROWSER_STATS_OBSERVED="${adapter_out}" \
|
|
89
|
+
stats_run_adapter_emit \
|
|
90
|
+
"click" "${tool_name}" "${stats_t0}" "${adapter_rc}" "${adapter_out}" "" \
|
|
91
|
+
-- "${verb_argv[@]}" || true
|
|
92
|
+
|
|
93
|
+
[ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
|
|
94
|
+
|
|
95
|
+
if [ "${adapter_rc}" -eq 0 ]; then
|
|
96
|
+
emit_summary verb=click tool="${tool_name}" why="${why}" status=ok ref="${ref}" selector="${selector}"
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
emit_summary verb=click tool="${tool_name}" why="${why}" status=error ref="${ref}" selector="${selector}"
|
|
100
|
+
exit "${adapter_rc}"
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# creds-add — register a credential. Smart per-OS backend select; AP-7 strict
|
|
3
|
+
# (password ALWAYS via stdin, never argv). Metadata in ${CREDENTIALS_DIR}/
|
|
4
|
+
# <name>.json mode 0600; secret payload via backend (plaintext: same dir,
|
|
5
|
+
# <name>.secret mode 0600; keychain/libsecret: OS vault).
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
IFS=$'\n\t'
|
|
8
|
+
umask 077
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
# shellcheck source=lib/common.sh
|
|
12
|
+
# shellcheck disable=SC1091
|
|
13
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
14
|
+
# shellcheck source=lib/site.sh
|
|
15
|
+
# shellcheck disable=SC1091
|
|
16
|
+
source "${SCRIPT_DIR}/lib/site.sh"
|
|
17
|
+
# shellcheck source=lib/credential.sh
|
|
18
|
+
# shellcheck disable=SC1091
|
|
19
|
+
source "${SCRIPT_DIR}/lib/credential.sh"
|
|
20
|
+
# shellcheck source=lib/secret_backend_select.sh
|
|
21
|
+
# shellcheck disable=SC1091
|
|
22
|
+
source "${SCRIPT_DIR}/lib/secret_backend_select.sh"
|
|
23
|
+
init_paths
|
|
24
|
+
|
|
25
|
+
site=""
|
|
26
|
+
as=""
|
|
27
|
+
account=""
|
|
28
|
+
backend=""
|
|
29
|
+
auto_relogin="true"
|
|
30
|
+
auth_flow="single-step-username-password"
|
|
31
|
+
enable_totp=0
|
|
32
|
+
yes_totp=0
|
|
33
|
+
totp_secret_stdin=0
|
|
34
|
+
read_stdin=0
|
|
35
|
+
dry_run=0
|
|
36
|
+
yes_plaintext=0
|
|
37
|
+
|
|
38
|
+
usage() {
|
|
39
|
+
cat <<'USAGE'
|
|
40
|
+
Usage: creds-add --site SITE --as CRED_NAME --password-stdin [options]
|
|
41
|
+
|
|
42
|
+
--site SITE site profile name (must exist)
|
|
43
|
+
--as CRED_NAME credential name (filename, must be safe)
|
|
44
|
+
--account ACCOUNT account/email value (default: "<site>@example.com")
|
|
45
|
+
--backend BACKEND keychain | libsecret | plaintext
|
|
46
|
+
(default: smart auto-detect per OS — keychain on
|
|
47
|
+
Darwin, libsecret on Linux when secret-tool is
|
|
48
|
+
reachable, plaintext fallback otherwise)
|
|
49
|
+
--auto-relogin BOOL true | false (default: true; relogin via login --auto
|
|
50
|
+
requires auth_flow=single-step-username-password)
|
|
51
|
+
--auth-flow FLOW single-step-username-password | multi-step-username-
|
|
52
|
+
password | username-only | custom (default:
|
|
53
|
+
single-step-username-password). Only single-step is
|
|
54
|
+
supported by login --auto today; others persist
|
|
55
|
+
metadata for documentation but require --interactive
|
|
56
|
+
for relogin.
|
|
57
|
+
--enable-totp mark this credential as TOTP-enabled (phase-5 part
|
|
58
|
+
4-i: plumbing only — codegen/replay/rotation land
|
|
59
|
+
in parts 4-ii/iii/iv). Requires --yes-i-know-totp
|
|
60
|
+
(typed-phrase ack) and forbids --backend plaintext
|
|
61
|
+
(TOTP shared secrets MUST go through OS keychain /
|
|
62
|
+
libsecret per parent spec §1).
|
|
63
|
+
--yes-i-know-totp acknowledgment for --enable-totp.
|
|
64
|
+
--totp-secret-stdin read base32 TOTP shared secret from stdin AFTER
|
|
65
|
+
the password (separated by NUL byte). Stored at
|
|
66
|
+
the <name>:totp backend slot. Requires
|
|
67
|
+
--enable-totp. Phase-5 part 4-ii.
|
|
68
|
+
--password-stdin REQUIRED — read password from stdin (one line);
|
|
69
|
+
this is the ONLY password-input path. AP-7
|
|
70
|
+
forbids accepting the password as an argv arg.
|
|
71
|
+
--yes-i-know-plaintext acknowledge that the plaintext backend stores
|
|
72
|
+
the secret on disk. Required on the FIRST
|
|
73
|
+
plaintext credential add; subsequent adds skip
|
|
74
|
+
(a marker file at ${CREDENTIALS_DIR}/.plaintext-
|
|
75
|
+
acknowledged tracks acknowledgment).
|
|
76
|
+
--dry-run print planned action; write nothing
|
|
77
|
+
-h, --help this message
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
printf 'mypass' | creds-add --site prod --as prod--admin --password-stdin
|
|
81
|
+
printf 'mypass' | creds-add --site prod --as prod--admin --backend keychain --password-stdin
|
|
82
|
+
USAGE
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
while [ $# -gt 0 ]; do
|
|
86
|
+
case "$1" in
|
|
87
|
+
--site) site="$2"; shift 2 ;;
|
|
88
|
+
--as) as="$2"; shift 2 ;;
|
|
89
|
+
--account) account="$2"; shift 2 ;;
|
|
90
|
+
--backend) backend="$2"; shift 2 ;;
|
|
91
|
+
--auto-relogin) auto_relogin="$2"; shift 2 ;;
|
|
92
|
+
--auth-flow) auth_flow="$2"; shift 2 ;;
|
|
93
|
+
--enable-totp) enable_totp=1; shift ;;
|
|
94
|
+
--yes-i-know-totp) yes_totp=1; shift ;;
|
|
95
|
+
--totp-secret-stdin) totp_secret_stdin=1; shift ;;
|
|
96
|
+
--password-stdin) read_stdin=1; shift ;;
|
|
97
|
+
--yes-i-know-plaintext) yes_plaintext=1; shift ;;
|
|
98
|
+
--dry-run) dry_run=1; shift ;;
|
|
99
|
+
-h|--help) usage; exit 0 ;;
|
|
100
|
+
*) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
|
|
101
|
+
esac
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
[ -n "${site}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--site is required"; }
|
|
105
|
+
[ -n "${as}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--as is required"; }
|
|
106
|
+
[ "${read_stdin}" = "1" ] || { usage; die "${EXIT_USAGE_ERROR}" "--password-stdin is required (passwords MUST come via stdin per AP-7)"; }
|
|
107
|
+
|
|
108
|
+
case "${auto_relogin}" in
|
|
109
|
+
true|false) ;;
|
|
110
|
+
*) die "${EXIT_USAGE_ERROR}" "--auto-relogin must be 'true' or 'false' (got: ${auto_relogin})" ;;
|
|
111
|
+
esac
|
|
112
|
+
|
|
113
|
+
case "${auth_flow}" in
|
|
114
|
+
single-step-username-password|multi-step-username-password|username-only|custom) ;;
|
|
115
|
+
*) die "${EXIT_USAGE_ERROR}" "--auth-flow must be one of {single-step-username-password, multi-step-username-password, username-only, custom} (got: ${auth_flow})" ;;
|
|
116
|
+
esac
|
|
117
|
+
|
|
118
|
+
# Phase 5 part 4-i: --enable-totp requires explicit ack + forbids plaintext.
|
|
119
|
+
# Per parent spec §1, TOTP shared secrets are even more sensitive than
|
|
120
|
+
# passwords (they generate codes for the lifetime of the secret).
|
|
121
|
+
if [ "${enable_totp}" = "1" ] && [ "${yes_totp}" = "0" ]; then
|
|
122
|
+
die "${EXIT_USAGE_ERROR}" "--enable-totp requires --yes-i-know-totp (TOTP shared secrets are highly sensitive)"
|
|
123
|
+
fi
|
|
124
|
+
if [ "${totp_secret_stdin}" = "1" ] && [ "${enable_totp}" = "0" ]; then
|
|
125
|
+
die "${EXIT_USAGE_ERROR}" "--totp-secret-stdin requires --enable-totp"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
assert_safe_name "${as}" "credential-name"
|
|
129
|
+
|
|
130
|
+
# Phase 5 part 4-ii: prevent collisions with internal TOTP slot names.
|
|
131
|
+
# Internal slots use `<as>__totp` suffix; if a user picks a cred name ending
|
|
132
|
+
# in `__totp`, that user's password slot would alias another cred's TOTP slot.
|
|
133
|
+
case "${as}" in
|
|
134
|
+
*__totp) die "${EXIT_USAGE_ERROR}" "credential name '${as}' reserved suffix '__totp' (collides with TOTP slot of cred '${as%__totp}')" ;;
|
|
135
|
+
esac
|
|
136
|
+
[ -z "${account}" ] && account="${site}@example.com"
|
|
137
|
+
|
|
138
|
+
if ! site_exists "${site}"; then
|
|
139
|
+
die "${EXIT_SITE_NOT_FOUND}" "site '${site}' not registered (try: add-site --name ${site} --url ...)"
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
if credential_exists "${as}"; then
|
|
143
|
+
die "${EXIT_USAGE_ERROR}" "credential '${as}' already exists (run: creds-remove --as ${as} first)"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# Resolve backend.
|
|
147
|
+
if [ -z "${backend}" ]; then
|
|
148
|
+
backend="$(detect_backend)"
|
|
149
|
+
fi
|
|
150
|
+
case "${backend}" in
|
|
151
|
+
keychain|libsecret|plaintext) ;;
|
|
152
|
+
*) die "${EXIT_USAGE_ERROR}" "--backend must be one of {keychain, libsecret, plaintext} (got: ${backend})" ;;
|
|
153
|
+
esac
|
|
154
|
+
|
|
155
|
+
# Phase 5 part 4-i: TOTP-enabled creds MUST go through OS keychain / libsecret.
|
|
156
|
+
# plaintext on-disk storage of a TOTP shared secret means anyone with read
|
|
157
|
+
# access to the file can generate auth codes forever — that's worse than
|
|
158
|
+
# plaintext password (passwords expire/rotate; TOTP secrets typically don't).
|
|
159
|
+
if [ "${enable_totp}" = "1" ] && [ "${backend}" = "plaintext" ]; then
|
|
160
|
+
die "${EXIT_USAGE_ERROR}" "--enable-totp forbids --backend plaintext (TOTP secrets must go through keychain or libsecret)"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# First-use plaintext gate (per parent spec §1: plaintext is paper security
|
|
164
|
+
# without disk encryption — gate the first add behind an explicit ack).
|
|
165
|
+
# Marker file at ${CREDENTIALS_DIR}/.plaintext-acknowledged (mode 0600)
|
|
166
|
+
# tracks the user's acknowledgment; subsequent adds skip the gate silently.
|
|
167
|
+
if [ "${backend}" = "plaintext" ]; then
|
|
168
|
+
plaintext_marker="${CREDENTIALS_DIR}/.plaintext-acknowledged"
|
|
169
|
+
if [ ! -f "${plaintext_marker}" ]; then
|
|
170
|
+
if [ "${yes_plaintext}" -ne 1 ]; then
|
|
171
|
+
die "${EXIT_USAGE_ERROR}" \
|
|
172
|
+
"first plaintext credential requires --yes-i-know-plaintext (or pre-create ${plaintext_marker}); plaintext stores the secret on disk and is paper security without disk encryption — see 'doctor' for FileVault/LUKS status"
|
|
173
|
+
fi
|
|
174
|
+
mkdir -p "${CREDENTIALS_DIR}"
|
|
175
|
+
chmod 700 "${CREDENTIALS_DIR}"
|
|
176
|
+
( umask 077; : > "${plaintext_marker}" )
|
|
177
|
+
chmod 600 "${plaintext_marker}"
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# Read stdin. When --totp-secret-stdin is set, stdin is `password\0totp_secret`
|
|
182
|
+
# (NUL-separated, AP-7: secrets never on argv); otherwise it's just the password.
|
|
183
|
+
# Bash `$(cat)` strips embedded NULs ("warning: ignored null byte"), so use
|
|
184
|
+
# `read -r -d ''` which reads up to a NUL delimiter without losing bytes.
|
|
185
|
+
totp_secret=""
|
|
186
|
+
if [ "${totp_secret_stdin}" = "1" ]; then
|
|
187
|
+
IFS= read -r -d '' password || \
|
|
188
|
+
die "${EXIT_USAGE_ERROR}" "--totp-secret-stdin: stdin must be 'password\\0totp_secret' (no NUL found)"
|
|
189
|
+
# Second chunk: EOF-terminated (no trailing NUL required); `read` returns
|
|
190
|
+
# non-zero on EOF-before-delim but still populates the variable.
|
|
191
|
+
IFS= read -r -d '' totp_secret || true
|
|
192
|
+
if [ -z "${totp_secret}" ]; then
|
|
193
|
+
die "${EXIT_USAGE_ERROR}" "--totp-secret-stdin: stdin must be 'password\\0totp_secret' (got only one chunk)"
|
|
194
|
+
fi
|
|
195
|
+
else
|
|
196
|
+
password="$(cat)"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
started_at_ms="$(now_ms)"
|
|
200
|
+
|
|
201
|
+
if [ "${dry_run}" -eq 1 ]; then
|
|
202
|
+
ok "dry-run: would write ${CREDENTIALS_DIR}/${as}.{json,secret} via backend=${backend}"
|
|
203
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
204
|
+
summary_json verb=creds-add tool=none why=dry-run status=ok would_run=true \
|
|
205
|
+
credential="${as}" site="${site}" backend="${backend}" \
|
|
206
|
+
duration_ms="${duration_ms}"
|
|
207
|
+
exit "${EXIT_OK}"
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
now_ts="$(now_iso)"
|
|
211
|
+
totp_json="$([ "${enable_totp}" = "1" ] && printf 'true' || printf 'false')"
|
|
212
|
+
meta_json="$(jq -nc \
|
|
213
|
+
--arg n "${as}" \
|
|
214
|
+
--arg s "${site}" \
|
|
215
|
+
--arg a "${account}" \
|
|
216
|
+
--arg b "${backend}" \
|
|
217
|
+
--argjson ar "${auto_relogin}" \
|
|
218
|
+
--arg af "${auth_flow}" \
|
|
219
|
+
--argjson tt "${totp_json}" \
|
|
220
|
+
--arg now "${now_ts}" \
|
|
221
|
+
'{
|
|
222
|
+
schema_version: 1,
|
|
223
|
+
name: $n,
|
|
224
|
+
site: $s,
|
|
225
|
+
account: $a,
|
|
226
|
+
backend: $b,
|
|
227
|
+
auth_flow: $af,
|
|
228
|
+
auto_relogin: $ar,
|
|
229
|
+
totp_enabled: $tt,
|
|
230
|
+
created_at: $now
|
|
231
|
+
}')"
|
|
232
|
+
|
|
233
|
+
credential_save "${as}" "${meta_json}"
|
|
234
|
+
|
|
235
|
+
# Pipe the password into the backend via stdin (AP-7 — never argv).
|
|
236
|
+
printf '%s' "${password}" | credential_set_secret "${as}"
|
|
237
|
+
|
|
238
|
+
# Phase 5 part 4-ii: store TOTP shared secret in the same backend at the
|
|
239
|
+
# `<as>:totp` slot when --totp-secret-stdin was provided.
|
|
240
|
+
if [ -n "${totp_secret}" ]; then
|
|
241
|
+
printf '%s' "${totp_secret}" | credential_set_totp_secret "${as}"
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
if [ "${backend}" = "plaintext" ]; then
|
|
245
|
+
warn "credential '${as}' stored via plaintext backend at ${CREDENTIALS_DIR}/${as}.secret (mode 0600)"
|
|
246
|
+
warn " ensure disk encryption is enabled (FileVault/LUKS) — see 'doctor'"
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
ok "credential added: ${as} (site=${site}, backend=${backend})"
|
|
250
|
+
|
|
251
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
252
|
+
summary_json verb=creds-add tool=none why=register-credential status=ok \
|
|
253
|
+
credential="${as}" site="${site}" backend="${backend}" \
|
|
254
|
+
duration_ms="${duration_ms}"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# creds-list — list registered credentials. Optional --site filter mirrors
|
|
3
|
+
# list-sessions. Emits ONLY metadata (NEVER secret values; backend payloads
|
|
4
|
+
# stay in their respective vaults).
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
IFS=$'\n\t'
|
|
8
|
+
umask 077
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
# shellcheck source=lib/common.sh
|
|
12
|
+
# shellcheck disable=SC1091
|
|
13
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
14
|
+
# shellcheck source=lib/credential.sh
|
|
15
|
+
# shellcheck disable=SC1091
|
|
16
|
+
source "${SCRIPT_DIR}/lib/credential.sh"
|
|
17
|
+
init_paths
|
|
18
|
+
|
|
19
|
+
site_filter=""
|
|
20
|
+
while [ $# -gt 0 ]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--site)
|
|
23
|
+
site_filter="$2"; shift 2
|
|
24
|
+
;;
|
|
25
|
+
-h|--help)
|
|
26
|
+
cat <<'USAGE'
|
|
27
|
+
Usage: creds-list [--site NAME]
|
|
28
|
+
|
|
29
|
+
--site NAME show only credentials bound to this site
|
|
30
|
+
USAGE
|
|
31
|
+
exit 0
|
|
32
|
+
;;
|
|
33
|
+
*) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
|
|
34
|
+
esac
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
started_at_ms="$(now_ms)"
|
|
38
|
+
|
|
39
|
+
rows='[]'
|
|
40
|
+
count=0
|
|
41
|
+
for name in $(credential_list_names); do
|
|
42
|
+
meta="$(credential_load "${name}" 2>/dev/null)" || continue
|
|
43
|
+
cred_site="$(printf '%s' "${meta}" | jq -r '.site // ""')"
|
|
44
|
+
if [ -n "${site_filter}" ] && [ "${cred_site}" != "${site_filter}" ]; then
|
|
45
|
+
continue
|
|
46
|
+
fi
|
|
47
|
+
rows="$(jq --arg n "${name}" --argjson m "${meta}" '
|
|
48
|
+
. + [{
|
|
49
|
+
credential: $n,
|
|
50
|
+
site: ($m.site // null),
|
|
51
|
+
account: ($m.account // null),
|
|
52
|
+
backend: ($m.backend // null),
|
|
53
|
+
auto_relogin: ($m.auto_relogin // null),
|
|
54
|
+
totp_enabled: ($m.totp_enabled // null),
|
|
55
|
+
created_at: ($m.created_at // null)
|
|
56
|
+
}]' <<< "${rows}")"
|
|
57
|
+
count=$((count + 1))
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
61
|
+
jq -cn --argjson r "${rows}" --argjson c "${count}" --argjson d "${duration_ms}" \
|
|
62
|
+
--arg sf "${site_filter}" \
|
|
63
|
+
'{verb: "creds-list", tool: "none",
|
|
64
|
+
why: (if $sf == "" then "list-all" else "list-by-site" end),
|
|
65
|
+
status: (if $c == 0 then "empty" else "ok" end),
|
|
66
|
+
site_filter: ($sf | if . == "" then null else . end),
|
|
67
|
+
count: $c, credentials: $r, duration_ms: $d}'
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# creds-migrate — move a credential from one backend to another. Fail-safe
|
|
3
|
+
# ordering: writes to new backend BEFORE deleting from old, so a failed
|
|
4
|
+
# new-backend write leaves the original credential intact.
|
|
5
|
+
#
|
|
6
|
+
# Inherits the first-use plaintext gate from creds-add: migrating TO plaintext
|
|
7
|
+
# requires --yes-i-know-plaintext (or a pre-existing acknowledgment marker)
|
|
8
|
+
# so users can't bypass the gate by going via creds-migrate.
|
|
9
|
+
#
|
|
10
|
+
# Privacy invariant: summary JSON NEVER contains the secret value.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
IFS=$'\n\t'
|
|
13
|
+
umask 077
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
# shellcheck source=lib/common.sh
|
|
17
|
+
# shellcheck disable=SC1091
|
|
18
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
19
|
+
# shellcheck source=lib/credential.sh
|
|
20
|
+
# shellcheck disable=SC1091
|
|
21
|
+
source "${SCRIPT_DIR}/lib/credential.sh"
|
|
22
|
+
init_paths
|
|
23
|
+
|
|
24
|
+
name=""; to_backend=""; yes=0; yes_plaintext=0; dry_run=0
|
|
25
|
+
|
|
26
|
+
usage() {
|
|
27
|
+
cat <<'USAGE'
|
|
28
|
+
Usage: creds-migrate --as CRED_NAME --to BACKEND [options]
|
|
29
|
+
|
|
30
|
+
--as CRED_NAME credential to migrate (required)
|
|
31
|
+
--to BACKEND target: keychain | libsecret | plaintext (required)
|
|
32
|
+
--yes-i-know skip the typed-name confirmation
|
|
33
|
+
--yes-i-know-plaintext acknowledge plaintext storage; required when
|
|
34
|
+
--to plaintext on a fresh box without the
|
|
35
|
+
${CREDENTIALS_DIR}/.plaintext-acknowledged marker
|
|
36
|
+
--dry-run print planned action; migrate nothing
|
|
37
|
+
-h, --help this message
|
|
38
|
+
|
|
39
|
+
Fail-safe: if the new-backend write fails (e.g. keychain unavailable), the
|
|
40
|
+
original credential is left intact. If the old-backend delete fails AFTER a
|
|
41
|
+
successful new-backend write, both backends transiently hold the secret —
|
|
42
|
+
verb logs a warning, doesn't crash; you can manually clean via creds-remove
|
|
43
|
+
on the old backend OR re-run creds-migrate to consolidate.
|
|
44
|
+
USAGE
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
while [ $# -gt 0 ]; do
|
|
48
|
+
case "$1" in
|
|
49
|
+
--as) name="$2"; shift 2 ;;
|
|
50
|
+
--to) to_backend="$2"; shift 2 ;;
|
|
51
|
+
--yes-i-know) yes=1; shift ;;
|
|
52
|
+
--yes-i-know-plaintext) yes_plaintext=1; shift ;;
|
|
53
|
+
--dry-run) dry_run=1; shift ;;
|
|
54
|
+
-h|--help) usage; exit 0 ;;
|
|
55
|
+
*) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
[ -n "${name}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--as is required"; }
|
|
60
|
+
[ -n "${to_backend}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--to is required"; }
|
|
61
|
+
assert_safe_name "${name}" "credential-name"
|
|
62
|
+
|
|
63
|
+
case "${to_backend}" in
|
|
64
|
+
keychain|libsecret|plaintext) ;;
|
|
65
|
+
*) die "${EXIT_USAGE_ERROR}" "--to must be one of {keychain, libsecret, plaintext} (got: ${to_backend})" ;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
if ! credential_exists "${name}"; then
|
|
69
|
+
die "${EXIT_SITE_NOT_FOUND}" "credential not found: ${name}"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
old_meta="$(credential_load "${name}")"
|
|
73
|
+
old_backend="$(printf '%s' "${old_meta}" | jq -r '.backend')"
|
|
74
|
+
|
|
75
|
+
if [ "${old_backend}" = "${to_backend}" ]; then
|
|
76
|
+
die "${EXIT_USAGE_ERROR}" "credential ${name}: already on backend '${to_backend}' (no-op refused)"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# First-use plaintext gate inherited from creds-add — migrating TO plaintext
|
|
80
|
+
# must respect the same acknowledgment requirement.
|
|
81
|
+
if [ "${to_backend}" = "plaintext" ]; then
|
|
82
|
+
plaintext_marker="${CREDENTIALS_DIR}/.plaintext-acknowledged"
|
|
83
|
+
if [ ! -f "${plaintext_marker}" ] && [ "${yes_plaintext}" -ne 1 ]; then
|
|
84
|
+
die "${EXIT_USAGE_ERROR}" \
|
|
85
|
+
"migrate-to-plaintext requires --yes-i-know-plaintext (or pre-create ${plaintext_marker}); plaintext stores the secret on disk and is paper security without disk encryption"
|
|
86
|
+
fi
|
|
87
|
+
# Touch the marker if not present (so subsequent plaintext ops skip the gate).
|
|
88
|
+
if [ ! -f "${plaintext_marker}" ]; then
|
|
89
|
+
mkdir -p "${CREDENTIALS_DIR}"
|
|
90
|
+
chmod 700 "${CREDENTIALS_DIR}"
|
|
91
|
+
( umask 077; : > "${plaintext_marker}" )
|
|
92
|
+
chmod 600 "${plaintext_marker}"
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
started_at_ms="$(now_ms)"
|
|
97
|
+
|
|
98
|
+
if [ "${dry_run}" -eq 1 ]; then
|
|
99
|
+
ok "dry-run: would migrate credential ${name}: ${old_backend} → ${to_backend}"
|
|
100
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
101
|
+
summary_json verb=creds-migrate tool=none why=dry-run status=ok would_run=true \
|
|
102
|
+
credential="${name}" from="${old_backend}" to="${to_backend}" \
|
|
103
|
+
duration_ms="${duration_ms}"
|
|
104
|
+
exit "${EXIT_OK}"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if [ "${yes}" -ne 1 ]; then
|
|
108
|
+
printf 'Type the credential name (%s) to confirm migration: ' "${name}" >&2
|
|
109
|
+
answer=""
|
|
110
|
+
IFS= read -r answer || true
|
|
111
|
+
if [ "${answer}" != "${name}" ]; then
|
|
112
|
+
die "${EXIT_USAGE_ERROR}" "migration aborted (confirmation mismatch)"
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
credential_migrate_to "${name}" "${to_backend}"
|
|
117
|
+
ok "credential migrated: ${name} (${old_backend} → ${to_backend})"
|
|
118
|
+
|
|
119
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
120
|
+
summary_json verb=creds-migrate tool=none why=migrate status=ok \
|
|
121
|
+
credential="${name}" from="${old_backend}" to="${to_backend}" \
|
|
122
|
+
duration_ms="${duration_ms}"
|