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,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-hover.sh — pointer hover an element by --ref eN or --selector CSS.
|
|
3
|
+
# Usage: bash scripts/browser-hover.sh [--site NAME] [--tool NAME] [--dry-run]
|
|
4
|
+
# [--raw] (--ref eN | --selector CSS)
|
|
5
|
+
#
|
|
6
|
+
# Routes to chrome-devtools-mcp by default (Phase 6 part 3). Stateful —
|
|
7
|
+
# requires running daemon (refMap precondition; mirrors click/select).
|
|
8
|
+
# --selector path enables Phase 11 cache dispatch (cache stores selectors,
|
|
9
|
+
# not snapshot-relative refs). Mirrors browser-click.sh + browser-fill.sh
|
|
10
|
+
# precedent.
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
IFS=$'\n\t'
|
|
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/output.sh
|
|
20
|
+
# shellcheck disable=SC1091
|
|
21
|
+
source "${SCRIPT_DIR}/lib/output.sh"
|
|
22
|
+
# shellcheck source=lib/router.sh
|
|
23
|
+
# shellcheck disable=SC1091
|
|
24
|
+
source "${SCRIPT_DIR}/lib/router.sh"
|
|
25
|
+
# shellcheck source=lib/verb_helpers.sh
|
|
26
|
+
# shellcheck disable=SC1091
|
|
27
|
+
source "${SCRIPT_DIR}/lib/verb_helpers.sh"
|
|
28
|
+
|
|
29
|
+
init_paths
|
|
30
|
+
|
|
31
|
+
SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
|
|
32
|
+
|
|
33
|
+
parse_verb_globals "$@"
|
|
34
|
+
|
|
35
|
+
resolve_session_storage_state
|
|
36
|
+
|
|
37
|
+
ref="" selector=""
|
|
38
|
+
verb_argv=()
|
|
39
|
+
i=0
|
|
40
|
+
while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
|
|
41
|
+
case "${REMAINING_ARGV[i]}" in
|
|
42
|
+
--ref)
|
|
43
|
+
ref="${REMAINING_ARGV[i+1]:-}"
|
|
44
|
+
[ -n "${ref}" ] || die "${EXIT_USAGE_ERROR}" "--ref requires a value"
|
|
45
|
+
verb_argv+=(--ref "${ref}")
|
|
46
|
+
i=$((i + 2))
|
|
47
|
+
;;
|
|
48
|
+
--selector)
|
|
49
|
+
selector="${REMAINING_ARGV[i+1]:-}"
|
|
50
|
+
[ -n "${selector}" ] || die "${EXIT_USAGE_ERROR}" "--selector requires a value"
|
|
51
|
+
verb_argv+=(--selector "${selector}")
|
|
52
|
+
i=$((i + 2))
|
|
53
|
+
;;
|
|
54
|
+
*)
|
|
55
|
+
verb_argv+=("${REMAINING_ARGV[i]}")
|
|
56
|
+
i=$((i + 1))
|
|
57
|
+
;;
|
|
58
|
+
esac
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
if [ -n "${ref}" ] && [ -n "${selector}" ]; then
|
|
62
|
+
die "${EXIT_USAGE_ERROR}" "--ref and --selector are mutually exclusive"
|
|
63
|
+
fi
|
|
64
|
+
if [ -z "${ref}" ] && [ -z "${selector}" ]; then
|
|
65
|
+
die "${EXIT_USAGE_ERROR}" "hover requires --ref eN or --selector CSS"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
|
|
69
|
+
ok "dry-run: would hover ${ref:-${selector}}"
|
|
70
|
+
emit_summary verb=hover tool=none why=dry-run status=ok ref="${ref}" selector="${selector}" dry_run=true
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
picked="$(pick_tool hover "${verb_argv[@]}")"
|
|
75
|
+
tool_name="${picked%%$'\t'*}"
|
|
76
|
+
why="${picked#*$'\t'}"
|
|
77
|
+
|
|
78
|
+
source_picked_adapter "${tool_name}"
|
|
79
|
+
|
|
80
|
+
set +e
|
|
81
|
+
adapter_out="$(invoke_with_retry hover "${verb_argv[@]}")"
|
|
82
|
+
adapter_rc=$?
|
|
83
|
+
set -e
|
|
84
|
+
|
|
85
|
+
[ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
|
|
86
|
+
|
|
87
|
+
if [ "${adapter_rc}" -eq 0 ]; then
|
|
88
|
+
emit_summary verb=hover tool="${tool_name}" why="${why}" status=ok ref="${ref}" selector="${selector}"
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
emit_summary verb=hover tool="${tool_name}" why="${why}" status=error ref="${ref}" selector="${selector}"
|
|
92
|
+
exit "${adapter_rc}"
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-inspect.sh — inspect a page (console, network, screenshot,
|
|
3
|
+
# or selector text). Usage:
|
|
4
|
+
# bash scripts/browser-inspect.sh [--site NAME] [--tool NAME] [--dry-run]
|
|
5
|
+
# [--raw]
|
|
6
|
+
# (--capture-console | --capture-network
|
|
7
|
+
# | --screenshot | --selector CSS)
|
|
8
|
+
# [--capture]
|
|
9
|
+
#
|
|
10
|
+
# Routes to chrome-devtools-mcp by default (post-1d router promotion — only
|
|
11
|
+
# adapter with dedicated console + network MCP tools per parent spec
|
|
12
|
+
# Appendix B). At least one of --capture-* / --screenshot / --selector is
|
|
13
|
+
# required so the adapter has something to do.
|
|
14
|
+
#
|
|
15
|
+
# Phase 7 part 1-iii: --capture writes adapter output to ${CAPTURES_DIR}/NNN/
|
|
16
|
+
# as console.json + network.har, sanitized via lib/sanitize.sh. Stdout output
|
|
17
|
+
# is ALSO sanitized (defense in depth) — single transformation, both sinks.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
IFS=$'\n\t'
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
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
|
+
# shellcheck source=lib/router.sh
|
|
30
|
+
# shellcheck disable=SC1091
|
|
31
|
+
source "${SCRIPT_DIR}/lib/router.sh"
|
|
32
|
+
# shellcheck source=lib/verb_helpers.sh
|
|
33
|
+
# shellcheck disable=SC1091
|
|
34
|
+
source "${SCRIPT_DIR}/lib/verb_helpers.sh"
|
|
35
|
+
# shellcheck source=lib/capture.sh
|
|
36
|
+
# shellcheck disable=SC1091
|
|
37
|
+
source "${SCRIPT_DIR}/lib/capture.sh"
|
|
38
|
+
# shellcheck source=lib/sanitize.sh
|
|
39
|
+
# shellcheck disable=SC1091
|
|
40
|
+
source "${SCRIPT_DIR}/lib/sanitize.sh"
|
|
41
|
+
|
|
42
|
+
init_paths
|
|
43
|
+
|
|
44
|
+
SUMMARY_T0="$(now_ms)"; export SUMMARY_T0
|
|
45
|
+
|
|
46
|
+
parse_verb_globals "$@"
|
|
47
|
+
|
|
48
|
+
resolve_session_storage_state
|
|
49
|
+
|
|
50
|
+
selector="" capture_console=0 capture_network=0 screenshot=0 do_capture=0 unsanitized=0
|
|
51
|
+
verb_argv=()
|
|
52
|
+
i=0
|
|
53
|
+
while [ "${i}" -lt "${#REMAINING_ARGV[@]}" ]; do
|
|
54
|
+
case "${REMAINING_ARGV[i]}" in
|
|
55
|
+
--selector)
|
|
56
|
+
selector="${REMAINING_ARGV[i+1]:-}"
|
|
57
|
+
[ -n "${selector}" ] || die "${EXIT_USAGE_ERROR}" "--selector requires a value"
|
|
58
|
+
verb_argv+=(--selector "${selector}")
|
|
59
|
+
i=$((i + 2))
|
|
60
|
+
;;
|
|
61
|
+
--capture-console)
|
|
62
|
+
capture_console=1
|
|
63
|
+
verb_argv+=(--capture-console)
|
|
64
|
+
i=$((i + 1))
|
|
65
|
+
;;
|
|
66
|
+
--capture-network)
|
|
67
|
+
capture_network=1
|
|
68
|
+
verb_argv+=(--capture-network)
|
|
69
|
+
i=$((i + 1))
|
|
70
|
+
;;
|
|
71
|
+
--screenshot)
|
|
72
|
+
screenshot=1
|
|
73
|
+
verb_argv+=(--screenshot)
|
|
74
|
+
i=$((i + 1))
|
|
75
|
+
;;
|
|
76
|
+
--capture)
|
|
77
|
+
do_capture=1
|
|
78
|
+
i=$((i + 1))
|
|
79
|
+
;;
|
|
80
|
+
--unsanitized)
|
|
81
|
+
unsanitized=1
|
|
82
|
+
i=$((i + 1))
|
|
83
|
+
;;
|
|
84
|
+
*)
|
|
85
|
+
verb_argv+=("${REMAINING_ARGV[i]}")
|
|
86
|
+
i=$((i + 1))
|
|
87
|
+
;;
|
|
88
|
+
esac
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
# Phase 7 part 1-iv: --unsanitized requires typed-phrase confirmation.
|
|
92
|
+
# Strict equality (no whitespace strip) — friction-by-design. Mirrors
|
|
93
|
+
# scripts/browser-creds-show.sh::--reveal precedent. Phrase verbatim per
|
|
94
|
+
# parent spec §8.3. Scripted use: pipe phrase via stdin.
|
|
95
|
+
if [ "${unsanitized}" = "1" ]; then
|
|
96
|
+
printf 'Type the unsanitized confirmation phrase to confirm: ' >&2
|
|
97
|
+
IFS= read -r unsanitized_answer || true
|
|
98
|
+
if [ "${unsanitized_answer}" != "I want raw network/console data including auth tokens" ]; then
|
|
99
|
+
die "${EXIT_USAGE_ERROR}" "unsanitized aborted (confirmation mismatch)"
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
if [ -z "${selector}" ] && [ "${capture_console}" = 0 ] \
|
|
104
|
+
&& [ "${capture_network}" = 0 ] && [ "${screenshot}" = 0 ]; then
|
|
105
|
+
die "${EXIT_USAGE_ERROR}" \
|
|
106
|
+
"inspect requires one of --capture-console / --capture-network / --screenshot / --selector CSS"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [ "${ARG_DRY_RUN:-0}" = "1" ]; then
|
|
110
|
+
ok "dry-run: would inspect (${selector:-<no-selector>})"
|
|
111
|
+
if [ "${do_capture}" = "1" ]; then
|
|
112
|
+
emit_summary verb=inspect tool=none why=dry-run status=ok selector="${selector}" dry_run=true capture=true
|
|
113
|
+
else
|
|
114
|
+
emit_summary verb=inspect tool=none why=dry-run status=ok selector="${selector}" dry_run=true
|
|
115
|
+
fi
|
|
116
|
+
exit 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
picked="$(pick_tool inspect "${verb_argv[@]}")"
|
|
120
|
+
tool_name="${picked%%$'\t'*}"
|
|
121
|
+
why="${picked#*$'\t'}"
|
|
122
|
+
|
|
123
|
+
source_picked_adapter "${tool_name}"
|
|
124
|
+
|
|
125
|
+
if [ "${do_capture}" = "1" ]; then
|
|
126
|
+
capture_start inspect
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
set +e
|
|
130
|
+
adapter_out="$(invoke_with_retry inspect "${verb_argv[@]}")"
|
|
131
|
+
adapter_rc=$?
|
|
132
|
+
set -e
|
|
133
|
+
|
|
134
|
+
if [ "${do_capture}" = "1" ] && [ -n "${adapter_out}" ]; then
|
|
135
|
+
# Single (maybe-)sanitize, both sinks: stdout + per-aspect files. Either
|
|
136
|
+
# path produces the same emit-twice contract; only the transformation
|
|
137
|
+
# differs. --unsanitized skips sanitize_inspect_reply.
|
|
138
|
+
if [ "${unsanitized}" = "1" ]; then
|
|
139
|
+
out_for_emit="${adapter_out}"
|
|
140
|
+
sanitized_flag=false
|
|
141
|
+
else
|
|
142
|
+
out_for_emit="$(printf '%s' "${adapter_out}" | sanitize_inspect_reply)"
|
|
143
|
+
sanitized_flag=true
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# console.json — extract .console_messages array.
|
|
147
|
+
if [ "${capture_console}" = "1" ]; then
|
|
148
|
+
if printf '%s' "${out_for_emit}" | jq -e 'has("console_messages")' >/dev/null 2>&1; then
|
|
149
|
+
printf '%s' "${out_for_emit}" | jq '.console_messages // []' > "${CAPTURE_DIR}/console.json"
|
|
150
|
+
chmod 600 "${CAPTURE_DIR}/console.json"
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# network.har — wrap .network_requests in HAR envelope and persist.
|
|
155
|
+
if [ "${capture_network}" = "1" ]; then
|
|
156
|
+
if printf '%s' "${out_for_emit}" | jq -e 'has("network_requests")' >/dev/null 2>&1; then
|
|
157
|
+
printf '%s' "${out_for_emit}" \
|
|
158
|
+
| jq '{log: {version: "1.2", entries: (.network_requests // [])}}' \
|
|
159
|
+
> "${CAPTURE_DIR}/network.har"
|
|
160
|
+
chmod 600 "${CAPTURE_DIR}/network.har"
|
|
161
|
+
fi
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
printf '%s\n' "${out_for_emit}"
|
|
165
|
+
|
|
166
|
+
if [ "${adapter_rc}" -eq 0 ]; then
|
|
167
|
+
capture_finish ok "${sanitized_flag}"
|
|
168
|
+
else
|
|
169
|
+
capture_finish error "${sanitized_flag}"
|
|
170
|
+
fi
|
|
171
|
+
else
|
|
172
|
+
[ -n "${adapter_out}" ] && printf '%s\n' "${adapter_out}"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
if [ "${adapter_rc}" -eq 0 ]; then
|
|
176
|
+
if [ "${do_capture}" = "1" ]; then
|
|
177
|
+
emit_summary verb=inspect tool="${tool_name}" why="${why}" status=ok selector="${selector}" capture_id="${CAPTURE_ID}"
|
|
178
|
+
else
|
|
179
|
+
emit_summary verb=inspect tool="${tool_name}" why="${why}" status=ok selector="${selector}"
|
|
180
|
+
fi
|
|
181
|
+
exit 0
|
|
182
|
+
fi
|
|
183
|
+
if [ "${do_capture}" = "1" ]; then
|
|
184
|
+
emit_summary verb=inspect tool="${tool_name}" why="${why}" status=error selector="${selector}" capture_id="${CAPTURE_ID}"
|
|
185
|
+
else
|
|
186
|
+
emit_summary verb=inspect tool="${tool_name}" why="${why}" status=error selector="${selector}"
|
|
187
|
+
fi
|
|
188
|
+
exit "${adapter_rc}"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# list-sessions — list captured Playwright sessions (storageState files).
|
|
3
|
+
#
|
|
4
|
+
# Sessions are tied to sites via meta.site; pass --site NAME to filter.
|
|
5
|
+
# A site may have many sessions — this verb is the discoverability surface
|
|
6
|
+
# for the 1-many credential model (e.g. prod--admin, prod--readonly, prod--ci).
|
|
7
|
+
#
|
|
8
|
+
# Storage state itself is sensitive and stays at mode 0600 — this verb only
|
|
9
|
+
# emits metadata (origin, captured_at, expires_in_hours), never cookie/token
|
|
10
|
+
# values.
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
IFS=$'\n\t'
|
|
14
|
+
umask 077
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
# shellcheck source=lib/common.sh
|
|
18
|
+
# shellcheck disable=SC1091
|
|
19
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
20
|
+
# shellcheck source=lib/session.sh
|
|
21
|
+
# shellcheck disable=SC1091
|
|
22
|
+
source "${SCRIPT_DIR}/lib/session.sh"
|
|
23
|
+
init_paths
|
|
24
|
+
|
|
25
|
+
site_filter=""
|
|
26
|
+
while [ $# -gt 0 ]; do
|
|
27
|
+
case "$1" in
|
|
28
|
+
--site) site_filter="$2"; shift 2 ;;
|
|
29
|
+
-h|--help)
|
|
30
|
+
cat <<'USAGE'
|
|
31
|
+
Usage: list-sessions [--site NAME]
|
|
32
|
+
|
|
33
|
+
--site NAME show only sessions bound to this site
|
|
34
|
+
USAGE
|
|
35
|
+
exit 0
|
|
36
|
+
;;
|
|
37
|
+
*) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
|
|
38
|
+
esac
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
started_at_ms="$(now_ms)"
|
|
42
|
+
|
|
43
|
+
rows='[]'
|
|
44
|
+
count=0
|
|
45
|
+
if [ -d "${SESSIONS_DIR}" ]; then
|
|
46
|
+
shopt -s nullglob
|
|
47
|
+
for f in "${SESSIONS_DIR}"/*.json; do
|
|
48
|
+
base="$(basename "${f}" .json)"
|
|
49
|
+
case "${base}" in
|
|
50
|
+
*.meta) continue ;;
|
|
51
|
+
*interactive-tmp*) continue ;;
|
|
52
|
+
esac
|
|
53
|
+
[ -f "${SESSIONS_DIR}/${base}.meta.json" ] || continue
|
|
54
|
+
meta="$(session_meta_load "${base}" 2>/dev/null)" || continue
|
|
55
|
+
sess_site="$(printf '%s' "${meta}" | jq -r '.site // ""')"
|
|
56
|
+
if [ -n "${site_filter}" ] && [ "${sess_site}" != "${site_filter}" ]; then
|
|
57
|
+
continue
|
|
58
|
+
fi
|
|
59
|
+
rows="$(jq --arg n "${base}" --argjson m "${meta}" '
|
|
60
|
+
. + [{
|
|
61
|
+
session: $n,
|
|
62
|
+
site: ($m.site // null),
|
|
63
|
+
origin: ($m.origin // null),
|
|
64
|
+
captured_at: ($m.captured_at // null),
|
|
65
|
+
expires_in_hours: ($m.expires_in_hours // null)
|
|
66
|
+
}]' <<< "${rows}")"
|
|
67
|
+
count=$((count + 1))
|
|
68
|
+
done
|
|
69
|
+
shopt -u nullglob
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
73
|
+
jq -cn --argjson r "${rows}" --argjson c "${count}" --argjson d "${duration_ms}" \
|
|
74
|
+
--arg sf "${site_filter}" \
|
|
75
|
+
'{verb: "list-sessions", tool: "none", why: (if $sf == "" then "list-all" else "list-by-site" end),
|
|
76
|
+
status: (if $c == 0 then "empty" else "ok" end),
|
|
77
|
+
site_filter: ($sf | if . == "" then null else . end),
|
|
78
|
+
count: $c, sessions: $r, duration_ms: $d}'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# list-sites — list registered site profiles (no creds; sites are non-secret).
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
IFS=$'\n\t'
|
|
5
|
+
umask 077
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
# shellcheck source=lib/common.sh
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
11
|
+
# shellcheck source=lib/site.sh
|
|
12
|
+
# shellcheck disable=SC1091
|
|
13
|
+
source "${SCRIPT_DIR}/lib/site.sh"
|
|
14
|
+
init_paths
|
|
15
|
+
|
|
16
|
+
started_at_ms="$(now_ms)"
|
|
17
|
+
|
|
18
|
+
names="$(site_list_names)"
|
|
19
|
+
rows='[]'
|
|
20
|
+
count=0
|
|
21
|
+
if [ -n "${names}" ]; then
|
|
22
|
+
while IFS= read -r n; do
|
|
23
|
+
[ -z "${n}" ] && continue
|
|
24
|
+
profile="$(site_load "${n}")"
|
|
25
|
+
meta="$(site_meta_load "${n}")"
|
|
26
|
+
rows="$(jq --argjson p "${profile}" --argjson m "${meta}" '
|
|
27
|
+
. + [{
|
|
28
|
+
name: $p.name,
|
|
29
|
+
url: $p.url,
|
|
30
|
+
label: ($p.label // ""),
|
|
31
|
+
default_session:$p.default_session,
|
|
32
|
+
default_tool: $p.default_tool,
|
|
33
|
+
last_used_at: ($m.last_used_at // null)
|
|
34
|
+
}]' <<< "${rows}")"
|
|
35
|
+
count=$((count + 1))
|
|
36
|
+
done <<< "${names}"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
40
|
+
jq -cn --argjson r "${rows}" --argjson c "${count}" --argjson d "${duration_ms}" \
|
|
41
|
+
'{verb: "list-sites", tool: "none", why: "list", status: "ok",
|
|
42
|
+
count: $c, sites: $r, duration_ms: $d}'
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# login — capture a Playwright storageState into sessions/<name>.json.
|
|
3
|
+
#
|
|
4
|
+
# Three modes:
|
|
5
|
+
# --interactive headed Chromium; user logs in, presses Enter
|
|
6
|
+
# --storage-state-file import a hand-edited storageState file
|
|
7
|
+
# --auto phase-5 part 3 — programmatic headless login
|
|
8
|
+
# using the stored credential (creds-add). Reads
|
|
9
|
+
# username + password (NUL-separated) from
|
|
10
|
+
# credential, sends to driver via stdin per AP-7.
|
|
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/site.sh
|
|
20
|
+
# shellcheck disable=SC1091
|
|
21
|
+
source "${SCRIPT_DIR}/lib/site.sh"
|
|
22
|
+
# shellcheck source=lib/session.sh
|
|
23
|
+
# shellcheck disable=SC1091
|
|
24
|
+
source "${SCRIPT_DIR}/lib/session.sh"
|
|
25
|
+
# shellcheck source=lib/credential.sh
|
|
26
|
+
# shellcheck disable=SC1091
|
|
27
|
+
source "${SCRIPT_DIR}/lib/credential.sh"
|
|
28
|
+
init_paths
|
|
29
|
+
|
|
30
|
+
site=""; as=""; ss_file=""; dry_run=0; auto=0; headed=1; interactive=0
|
|
31
|
+
|
|
32
|
+
usage() {
|
|
33
|
+
cat <<'USAGE'
|
|
34
|
+
Usage: login --site NAME --as SESSION (--storage-state-file PATH | --interactive) [--dry-run]
|
|
35
|
+
|
|
36
|
+
Capture a Playwright storageState into sessions/<SESSION>.json. Two modes:
|
|
37
|
+
|
|
38
|
+
--interactive Launch a headed Chromium via the playwright-lib
|
|
39
|
+
driver. User logs in interactively; press Enter
|
|
40
|
+
in this terminal to capture and save the session.
|
|
41
|
+
--storage-state-file PATH Skip the browser launch; consume an already-
|
|
42
|
+
captured storageState file (legacy hand-edit
|
|
43
|
+
path, useful for CI / non-interactive imports).
|
|
44
|
+
--auto Programmatic headless login using the stored
|
|
45
|
+
credential (set via creds-add). Requires
|
|
46
|
+
--site + --as; the credential's auto_relogin
|
|
47
|
+
flag must be true. Username + password reach
|
|
48
|
+
the driver via stdin only (AP-7).
|
|
49
|
+
|
|
50
|
+
--site NAME site profile to bind the session to (required)
|
|
51
|
+
--as SESSION session name (required; falls back to site.default_session)
|
|
52
|
+
--dry-run validate inputs; write nothing
|
|
53
|
+
--headed accepted (interactive mode is always headed)
|
|
54
|
+
-h, --help
|
|
55
|
+
|
|
56
|
+
A site may have many sessions: pass different --as names to capture per-role
|
|
57
|
+
or per-account credentials (e.g. prod--admin, prod--readonly, prod--ci).
|
|
58
|
+
USAGE
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
while [ $# -gt 0 ]; do
|
|
62
|
+
case "$1" in
|
|
63
|
+
--site) site="$2"; shift 2 ;;
|
|
64
|
+
--as) as="$2"; shift 2 ;;
|
|
65
|
+
--storage-state-file) ss_file="$2"; shift 2 ;;
|
|
66
|
+
--interactive) interactive=1; shift ;;
|
|
67
|
+
--dry-run) dry_run=1; shift ;;
|
|
68
|
+
--headed) headed=1; shift ;;
|
|
69
|
+
--auto) auto=1; shift ;;
|
|
70
|
+
-h|--help) usage; exit 0 ;;
|
|
71
|
+
*) die "${EXIT_USAGE_ERROR}" "unknown flag: $1" ;;
|
|
72
|
+
esac
|
|
73
|
+
done
|
|
74
|
+
|
|
75
|
+
if [ "${auto}" -eq 1 ] && [ "${interactive}" -eq 1 ]; then
|
|
76
|
+
die "${EXIT_USAGE_ERROR}" "--auto and --interactive are mutually exclusive"
|
|
77
|
+
fi
|
|
78
|
+
if [ "${auto}" -eq 1 ] && [ -n "${ss_file}" ]; then
|
|
79
|
+
die "${EXIT_USAGE_ERROR}" "--auto and --storage-state-file are mutually exclusive"
|
|
80
|
+
fi
|
|
81
|
+
if [ "${interactive}" -eq 1 ] && [ -n "${ss_file}" ]; then
|
|
82
|
+
die "${EXIT_USAGE_ERROR}" "--interactive and --storage-state-file are mutually exclusive"
|
|
83
|
+
fi
|
|
84
|
+
[ -n "${site}" ] || { usage; die "${EXIT_USAGE_ERROR}" "--site is required"; }
|
|
85
|
+
# --as defaults to site.default_session if the site sets one.
|
|
86
|
+
if [ -z "${as}" ]; then
|
|
87
|
+
default_session_from_site="$(site_load "${site}" | jq -r '.default_session // ""')"
|
|
88
|
+
if [ -n "${default_session_from_site}" ]; then
|
|
89
|
+
as="${default_session_from_site}"
|
|
90
|
+
else
|
|
91
|
+
die "${EXIT_USAGE_ERROR}" "--as is required (site ${site} has no default_session set)"
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
assert_safe_name "${as}" "session-name"
|
|
95
|
+
if [ "${interactive}" -eq 0 ] && [ "${auto}" -eq 0 ] && [ -z "${ss_file}" ]; then
|
|
96
|
+
usage
|
|
97
|
+
die "${EXIT_USAGE_ERROR}" "--interactive, --auto, or --storage-state-file is required"
|
|
98
|
+
fi
|
|
99
|
+
if [ "${interactive}" -eq 0 ] && [ "${auto}" -eq 0 ]; then
|
|
100
|
+
[ -f "${ss_file}" ] || die "${EXIT_USAGE_ERROR}" "storage-state-file not found: ${ss_file}"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
started_at_ms="$(now_ms)"
|
|
104
|
+
|
|
105
|
+
# Site must exist; load its URL → derive origin for binding.
|
|
106
|
+
profile_json="$(site_load "${site}")" # exits 23 if missing
|
|
107
|
+
site_url="$(printf '%s' "${profile_json}" | jq -r .url)"
|
|
108
|
+
site_origin="$(url_origin "${site_url}")"
|
|
109
|
+
|
|
110
|
+
# --auto: programmatic headless login using stored credential. Validates the
|
|
111
|
+
# credential exists + is bound to this site + has auto_relogin=true. Loads
|
|
112
|
+
# the secret via credential_get_secret (dispatches to whichever backend the
|
|
113
|
+
# cred uses — plaintext / keychain / libsecret). Sends username\0password
|
|
114
|
+
# to the driver via stdin (AP-7 — secret never on argv).
|
|
115
|
+
if [ "${auto}" -eq 1 ]; then
|
|
116
|
+
if ! credential_exists "${as}"; then
|
|
117
|
+
die "${EXIT_SITE_NOT_FOUND}" "credential not found: ${as} (run: creds-add --site ${site} --as ${as} --password-stdin)"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
cred_meta="$(credential_load "${as}")"
|
|
121
|
+
cred_site="$(printf '%s' "${cred_meta}" | jq -r .site)"
|
|
122
|
+
cred_account="$(printf '%s' "${cred_meta}" | jq -r .account)"
|
|
123
|
+
cred_auto="$(printf '%s' "${cred_meta}" | jq -r .auto_relogin)"
|
|
124
|
+
cred_auth_flow="$(printf '%s' "${cred_meta}" | jq -r '.auth_flow // "single-step-username-password"')"
|
|
125
|
+
cred_totp_enabled="$(printf '%s' "${cred_meta}" | jq -r '.totp_enabled // false')"
|
|
126
|
+
|
|
127
|
+
if [ "${cred_site}" != "${site}" ]; then
|
|
128
|
+
die "${EXIT_USAGE_ERROR}" "credential ${as} is bound to site '${cred_site}', not '${site}'"
|
|
129
|
+
fi
|
|
130
|
+
if [ "${cred_auto}" != "true" ]; then
|
|
131
|
+
die "${EXIT_USAGE_ERROR}" "credential ${as} has auto_relogin=false; cannot --auto (re-add the credential or use --interactive)"
|
|
132
|
+
fi
|
|
133
|
+
if [ -z "${cred_account}" ] || [ "${cred_account}" = "null" ]; then
|
|
134
|
+
die "${EXIT_USAGE_ERROR}" "credential ${as} has empty account; cannot --auto"
|
|
135
|
+
fi
|
|
136
|
+
# Phase-5 part 3-iii: only single-step-username-password is supported by
|
|
137
|
+
# the playwright-driver auto-relogin path. Other auth_flow values were
|
|
138
|
+
# persisted at creds-add time for documentation; relogin requires user
|
|
139
|
+
# interaction.
|
|
140
|
+
if [ "${cred_auth_flow}" != "single-step-username-password" ]; then
|
|
141
|
+
die "${EXIT_USAGE_ERROR}" "credential ${as} has auth_flow=${cred_auth_flow}; --auto only supports single-step-username-password (use --interactive)"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
if [ "${dry_run}" -eq 1 ]; then
|
|
145
|
+
ok "dry-run: would auto-relogin ${as} (site=${site}, account=${cred_account})"
|
|
146
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
147
|
+
summary_json verb=login tool=playwright-lib why=auto-relogin-dry-run status=ok would_run=true \
|
|
148
|
+
site="${site}" session="${as}" account="${cred_account}" \
|
|
149
|
+
duration_ms="${duration_ms}"
|
|
150
|
+
exit "${EXIT_OK}"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
mkdir -p "${SESSIONS_DIR}"
|
|
154
|
+
chmod 700 "${SESSIONS_DIR}"
|
|
155
|
+
ss_file="${SESSIONS_DIR}/${as}.auto-tmp.$$"
|
|
156
|
+
ok "auto-relogin: launching headless Chromium at ${site_url} as ${cred_account}"
|
|
157
|
+
|
|
158
|
+
# Pipe `account\0password` to driver stdin. AP-7: secret never on argv.
|
|
159
|
+
# Phase-5 part 4-iii: when cred is totp_enabled, append `\0totp_secret` so
|
|
160
|
+
# the driver can replay TOTP automatically after detect2FA fires.
|
|
161
|
+
set +e
|
|
162
|
+
if [ "${cred_totp_enabled}" = "true" ]; then
|
|
163
|
+
{
|
|
164
|
+
printf '%s\0' "${cred_account}"
|
|
165
|
+
credential_get_secret "${as}"
|
|
166
|
+
printf '\0'
|
|
167
|
+
credential_get_totp_secret "${as}"
|
|
168
|
+
} | node "${SCRIPT_DIR}/lib/node/playwright-driver.mjs" auto-relogin \
|
|
169
|
+
--url "${site_url}" --output-path "${ss_file}"
|
|
170
|
+
else
|
|
171
|
+
{ printf '%s\0' "${cred_account}"; credential_get_secret "${as}"; } | \
|
|
172
|
+
node "${SCRIPT_DIR}/lib/node/playwright-driver.mjs" auto-relogin \
|
|
173
|
+
--url "${site_url}" --output-path "${ss_file}"
|
|
174
|
+
fi
|
|
175
|
+
driver_rc=${PIPESTATUS[1]}
|
|
176
|
+
set -e
|
|
177
|
+
if [ "${driver_rc}" = "${EXIT_AUTH_INTERACTIVE_REQUIRED}" ]; then
|
|
178
|
+
# Phase-5 part 3-iv: driver detected a 2FA challenge that it couldn't
|
|
179
|
+
# auto-replay (no totp_enabled cred OR replay failed). Tell the user to
|
|
180
|
+
# either store a TOTP secret (creds-add --enable-totp) or fall back to
|
|
181
|
+
# --interactive.
|
|
182
|
+
rm -f "${ss_file}"
|
|
183
|
+
die "${EXIT_AUTH_INTERACTIVE_REQUIRED}" \
|
|
184
|
+
"site requires 2FA / interactive challenge — re-run with --interactive (or store a TOTP secret with creds-add --enable-totp --totp-secret-stdin)"
|
|
185
|
+
fi
|
|
186
|
+
if [ "${driver_rc}" -ne 0 ]; then
|
|
187
|
+
rm -f "${ss_file}"
|
|
188
|
+
die "${EXIT_TOOL_CRASHED}" "auto-relogin failed (driver returned ${driver_rc})"
|
|
189
|
+
fi
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# Interactive mode: launch the driver, which opens a headed browser, waits
|
|
193
|
+
# for the user to press Enter, and writes the captured storageState to a
|
|
194
|
+
# temp file. Then we validate + save through the same pipeline as the
|
|
195
|
+
# storage-state-file path.
|
|
196
|
+
if [ "${interactive}" -eq 1 ]; then
|
|
197
|
+
if [ "${dry_run}" -eq 1 ]; then
|
|
198
|
+
ok "dry-run: would launch headed browser to ${site_url}, capture session ${as}"
|
|
199
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
200
|
+
summary_json verb=login tool=playwright-lib why=interactive-dry-run status=ok would_run=true \
|
|
201
|
+
site="${site}" session="${as}" duration_ms="${duration_ms}"
|
|
202
|
+
exit "${EXIT_OK}"
|
|
203
|
+
fi
|
|
204
|
+
# Tempfile under SESSIONS_DIR (mode 0600 inherited from 0700 dir + driver chmod).
|
|
205
|
+
mkdir -p "${SESSIONS_DIR}"
|
|
206
|
+
chmod 700 "${SESSIONS_DIR}"
|
|
207
|
+
ss_file="${SESSIONS_DIR}/${as}.interactive-tmp.$$"
|
|
208
|
+
ok "launching headed Chromium at ${site_url}; press Enter when done logging in"
|
|
209
|
+
if ! node "${SCRIPT_DIR}/lib/node/playwright-driver.mjs" login \
|
|
210
|
+
--url "${site_url}" --output-path "${ss_file}"; then
|
|
211
|
+
rm -f "${ss_file}"
|
|
212
|
+
die "${EXIT_TOOL_CRASHED}" "interactive login failed (driver returned non-zero)"
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# Read & validate the storageState file.
|
|
217
|
+
if ! ss_json="$(jq -c . "${ss_file}" 2>/dev/null)"; then
|
|
218
|
+
if [ "${interactive}" -eq 1 ] || [ "${auto}" -eq 1 ]; then
|
|
219
|
+
rm -f "${ss_file}"
|
|
220
|
+
fi
|
|
221
|
+
die "${EXIT_USAGE_ERROR}" "storage-state-file is not valid JSON: ${ss_file}"
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# Origin-binding (spec §5.5): every storageState.origins[] must match site_origin.
|
|
225
|
+
# Empty origins[] is allowed (storageState may carry only cookies).
|
|
226
|
+
mismatched="$(printf '%s' "${ss_json}" | jq -r --arg target "${site_origin}" '
|
|
227
|
+
[.origins[]? | select(.origin != $target) | .origin] | join(",")')"
|
|
228
|
+
if [ -n "${mismatched}" ]; then
|
|
229
|
+
die "${EXIT_SESSION_EXPIRED}" \
|
|
230
|
+
"origin mismatch: storage-state-file origins=[${mismatched}], site origin=${site_origin}"
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
ok "site=${site} session=${as} origin=${site_origin}"
|
|
234
|
+
|
|
235
|
+
if [ "${dry_run}" -eq 1 ]; then
|
|
236
|
+
ok "dry-run: would write ${SESSIONS_DIR}/${as}.json"
|
|
237
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
238
|
+
summary_json verb=login tool=playwright-lib why=dry-run status=ok would_run=true \
|
|
239
|
+
site="${site}" session="${as}" duration_ms="${duration_ms}"
|
|
240
|
+
exit "${EXIT_OK}"
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# Build meta sidecar.
|
|
244
|
+
captured_at="$(now_iso)"
|
|
245
|
+
if [ "${auto}" -eq 1 ]; then
|
|
246
|
+
ua_tag='browser-skill playwright-lib auto-relogin'
|
|
247
|
+
elif [ "${interactive}" -eq 1 ]; then
|
|
248
|
+
ua_tag='browser-skill playwright-lib interactive capture'
|
|
249
|
+
else
|
|
250
|
+
ua_tag='browser-skill storageState-file import'
|
|
251
|
+
fi
|
|
252
|
+
meta_json="$(jq -nc \
|
|
253
|
+
--arg n "${as}" \
|
|
254
|
+
--arg s "${site}" \
|
|
255
|
+
--arg o "${site_origin}" \
|
|
256
|
+
--arg c "${captured_at}" \
|
|
257
|
+
--arg ua "${ua_tag}" \
|
|
258
|
+
'{
|
|
259
|
+
name: $n, site: $s, origin: $o, captured_at: $c,
|
|
260
|
+
source_user_agent: $ua, expires_in_hours: 168, schema_version: 1
|
|
261
|
+
}')"
|
|
262
|
+
|
|
263
|
+
session_save "${as}" "${ss_json}" "${meta_json}"
|
|
264
|
+
if [ "${interactive}" -eq 1 ] || [ "${auto}" -eq 1 ]; then
|
|
265
|
+
rm -f "${ss_file}"
|
|
266
|
+
fi
|
|
267
|
+
ok "session captured: ${as}"
|
|
268
|
+
|
|
269
|
+
duration_ms=$(( $(now_ms) - started_at_ms ))
|
|
270
|
+
if [ "${auto}" -eq 1 ]; then
|
|
271
|
+
why_tag="auto-relogin"
|
|
272
|
+
elif [ "${interactive}" -eq 1 ]; then
|
|
273
|
+
why_tag="interactive-headed-capture"
|
|
274
|
+
else
|
|
275
|
+
why_tag="storageState-file-import"
|
|
276
|
+
fi
|
|
277
|
+
summary_json verb=login tool=playwright-lib why="${why_tag}" status=ok \
|
|
278
|
+
site="${site}" session="${as}" origin="${site_origin}" \
|
|
279
|
+
expires_in_hours=168 duration_ms="${duration_ms}"
|