secflow-cli 0.2.4__tar.gz → 0.3.0__tar.gz

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.
Files changed (47) hide show
  1. {secflow_cli-0.2.4/src/secflow_cli.egg-info → secflow_cli-0.3.0}/PKG-INFO +15 -14
  2. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/USAGE.md +13 -13
  3. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/pyproject.toml +3 -2
  4. secflow_cli-0.3.0/src/secflow_cli/__init__.py +1 -0
  5. secflow_cli-0.3.0/src/secflow_cli/__main__.py +3 -0
  6. secflow_cli-0.3.0/src/secflow_cli/bootstrap.py +37 -0
  7. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/browser_login.py +52 -14
  8. secflow_cli-0.3.0/src/secflow_cli/logkit/__init__.py +976 -0
  9. secflow_cli-0.3.0/src/secflow_cli/logkit/test_logkit.py +1282 -0
  10. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/main.py +10 -1
  11. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/manifest_commands.py +5 -0
  12. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/output.py +4 -0
  13. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/session_commands.py +8 -4
  14. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/token_commands.py +12 -4
  15. secflow_cli-0.3.0/src/secflow_cli/update_install.py +322 -0
  16. secflow_cli-0.3.0/src/secflow_cli/update_runtime.py +511 -0
  17. secflow_cli-0.3.0/src/secflow_cli/update_security.py +170 -0
  18. secflow_cli-0.3.0/src/secflow_cli/update_state.py +195 -0
  19. {secflow_cli-0.2.4 → secflow_cli-0.3.0/src/secflow_cli.egg-info}/PKG-INFO +15 -14
  20. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/SOURCES.txt +8 -1
  21. secflow_cli-0.3.0/src/secflow_cli.egg-info/entry_points.txt +2 -0
  22. secflow_cli-0.2.4/src/secflow_cli/__init__.py +0 -0
  23. secflow_cli-0.2.4/src/secflow_cli/__main__.py +0 -3
  24. secflow_cli-0.2.4/src/secflow_cli.egg-info/entry_points.txt +0 -2
  25. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/LICENSE +0 -0
  26. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/setup.cfg +0 -0
  27. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/access_profile_commands.py +0 -0
  28. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/account_commands.py +0 -0
  29. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/command_support.py +0 -0
  30. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/completion.py +0 -0
  31. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/config.py +0 -0
  32. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/credentials.py +0 -0
  33. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/doctor_commands.py +0 -0
  34. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/errors.py +0 -0
  35. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/help.py +0 -0
  36. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/namespace_commands.py +0 -0
  37. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/operator_commands.py +0 -0
  38. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/paging.py +0 -0
  39. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/policy_audit_commands.py +0 -0
  40. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/reference_commands.py +0 -0
  41. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/report_commands.py +0 -0
  42. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/role_commands.py +0 -0
  43. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/runtime_commands.py +0 -0
  44. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli/secret_commands.py +0 -0
  45. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/dependency_links.txt +0 -0
  46. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/requires.txt +0 -0
  47. {secflow_cli-0.2.4 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/top_level.txt +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secflow-cli
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: SecFlow operator CLI
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/yeon3724/secflow
7
7
  Project-URL: Issues, https://github.com/yeon3724/secflow/issues
8
+ Keywords: intflow-cli-update-protocol-1
8
9
  Requires-Python: >=3.10
9
10
  Description-Content-Type: text/markdown
10
11
  License-File: LICENSE
@@ -14,11 +15,11 @@ Dynamic: license-file
14
15
 
15
16
  # SecFlow CLI
16
17
 
17
- Use Intflow browser login or an existing Slack-issued Admin session. Both distributions provide the same `secflowctl` commands.
18
+ Use Intflow browser login or an Admin session token.
18
19
 
19
20
  ## Install and update
20
21
 
21
- Choose one distribution per machine; both own the same `secflowctl` command and share profiles and the OS credential store.
22
+ Choose npm or Python; both own `secflowctl` and share profiles and OS credentials.
22
23
 
23
24
  ```sh
24
25
  pipx install secflow-cli
@@ -30,19 +31,19 @@ npm install --global @intflow/secflow-cli
30
31
  secflowctl --version
31
32
  ```
32
33
 
33
- The Python distribution requires Python 3.10+. Installation automatically includes the matching Admin-capable `secflow-sdk` dependency.
34
+ Python 3.10+ is required; installation includes the pinned Admin-capable SDK.
34
35
 
35
- The npm distribution includes Python and the CLI dependencies in OS-specific native packages. Initial targets: Windows x64, macOS Intel/Apple Silicon, and Linux x64 with glibc 2.35+ (Ubuntu 22.04+). Alpine/musl and Linux ARM are not npm targets yet; use the Python distribution there. Keep npm optional dependencies enabled. Installation has no postinstall script or runtime download. Native binaries are not platform-signed or notarized.
36
+ The npm distribution bundles Python and dependencies for Windows x64, macOS Intel/Apple Silicon, and Linux x64 glibc 2.35+. Alpine/musl and Linux ARM use the Python distribution. Keep optional dependencies enabled; installation has no postinstall script. Releases require all four same-version native packages. Binaries are not platform-signed or notarized.
36
37
 
37
- The bundled interpreter includes CA certificates for HTTPS. An explicit `SSL_CERT_FILE` or `SSL_CERT_DIR` takes precedence for organization-managed trust; TLS certificate verification stays enabled.
38
+ The bundle includes HTTPS CAs; `SSL_CERT_FILE`/`SSL_CERT_DIR` overrides trust without disabling TLS verification.
38
39
 
39
40
  ```sh
40
- pipx upgrade secflow-cli
41
- # Or use the same installer as the initial installation:
42
- python -m pip install --upgrade secflow-cli
43
- npm install --global @intflow/secflow-cli@latest
41
+ secflowctl update --check
42
+ secflowctl update --status
44
43
  ```
45
44
 
45
+ Commands check within 3s and stage protocol-1 stable runtimes in a private cache/venv, then run argv once; offline failures keep the last good runtime. TTY/JSON agree; help/version/check/status never install. `update --apply` installs; `--auto on|off` persists policy; `--pin VERSION` freezes an exact runtime; `--unpin` releases only policy. `INTFLOW_CLI_AUTO_UPDATE=0` stops automatic installs, allowing explicit apply/pin. Pins outrank off and fail closed. Internal native binaries update through npm.
46
+
46
47
  ## Connect
47
48
 
48
49
  ```sh
@@ -55,9 +56,9 @@ secflowctl --help
55
56
 
56
57
  `secflowctl` without arguments displays a quick start. `secflowctl login` opens Intflow login and creates the `production` profile for `https://secflow.intflow.dev` on first use. Existing profiles retain their selected server. For another server, first use `secflowctl profile add staging --base-url https://secflow.example.test`, then `secflowctl --profile staging login`. Global options precede the command. Every command supports `--help`, including examples. Account and role administration requires server authorization.
57
58
 
58
- `auth login` remains compatible with Slack-issued Admin sessions and prompts without echo for an existing session. Sessions are stored in Windows Credential Manager, macOS Keychain, or Linux Secret Service. Linux persistent login needs an unlocked Secret Service (for example GNOME Keyring) and a D-Bus session. If secure storage is unavailable, login fails without a plaintext file fallback. Installing the CLI does not create an account or grant access.
59
+ `auth login` prompts without echo for an existing Admin session token. Sessions are stored in Windows Credential Manager, macOS Keychain, or Linux Secret Service. Linux persistent login needs an unlocked Secret Service (for example GNOME Keyring) and a D-Bus session. If secure storage is unavailable, login fails without a plaintext file fallback. Installing the CLI does not create an account or grant access.
59
60
 
60
- When browser login is enabled on the server, run `secflowctl auth login --browser`. Sign in to Intflow, compare the code with your terminal, and approve the SecFlow login. Use `--browser --no-browser` to open the displayed URL manually. This requires an interactive terminal, an existing authorized SecFlow account, and OS credential storage. The CLI stores only the resulting SecFlow session. If login expires or the exchange fails, start a new login.
61
+ When browser login is enabled on the server, run `secflowctl auth login --browser`. Sign in to Intflow, compare the code with your terminal, and approve the SecFlow login. Use `--browser --no-browser` to open the displayed URL manually. This requires an interactive terminal, an existing authorized SecFlow account, and OS credential storage. The CLI stores only the resulting SecFlow session. A busy server is retried for a minute. If login expires or the exchange fails, log in again.
61
62
 
62
63
  For automation use `SECFLOW_ADMIN_SESSION_TOKEN` supplied by your secret manager, or `--token-stdin` with `--non-interactive`. Never put tokens in command arguments, profiles, shell history, or tracked files. SDK tokens cannot authorize these Admin commands. `auth logout` revokes the current server session; `auth forget` only removes the local credential.
63
64
 
@@ -128,6 +129,6 @@ For automation, use `--non-interactive` before the command and `--yes` on `put`.
128
129
 
129
130
  ## Credentials and values
130
131
 
131
- `tokens issue --save-as NAME` stores an SDK token separately from Admin profiles; `runtime ... --sdk-name NAME` uses it. `auth sessions issue --save-as PROFILE` creates a new destination profile. Both require a new destination name or explicit `--token-stdout` to redirected stdout; raw credentials never appear in ordinary JSON/table results. Linux/macOS non-interactive invocations require an explicitly injected credential rather than unlocking the OS store.
132
+ `tokens issue` uses the server-default lifetime; choose `--ttl-seconds` (1-7776000) or `--no-expiry`, never both. `--save-as NAME` stores an SDK token for `runtime ... --sdk-name NAME`. `auth sessions issue --save-as PROFILE` stores a separate Admin profile. Both need a new destination or `--token-stdout` redirected to a pipe/file; ordinary results contain no raw credentials. Linux/macOS non-interactive calls need an injected credential, not an OS store unlock.
132
133
 
133
- Secret create/rotate reads exact UTF-8 or base64 input through hidden input, `--value-stdin`, or `--value-file`. Do not put values in command arguments. Stdin cannot simultaneously carry an Admin token and a secret or policy document. Runtime commands use SDK credentials only; Admin sessions are never substituted. No plaintext resolve command is provided.
134
+ Secret create/rotate accepts exact UTF-8 or base64 via hidden input, `--value-stdin` or `--value-file`, never command arguments. Stdin cannot carry both an Admin token and a value or policy. Runtime uses SDK credentials, never Admin sessions; no plaintext resolve command exists.
@@ -1,10 +1,10 @@
1
1
  # SecFlow CLI
2
2
 
3
- Use Intflow browser login or an existing Slack-issued Admin session. Both distributions provide the same `secflowctl` commands.
3
+ Use Intflow browser login or an Admin session token.
4
4
 
5
5
  ## Install and update
6
6
 
7
- Choose one distribution per machine; both own the same `secflowctl` command and share profiles and the OS credential store.
7
+ Choose npm or Python; both own `secflowctl` and share profiles and OS credentials.
8
8
 
9
9
  ```sh
10
10
  pipx install secflow-cli
@@ -16,19 +16,19 @@ npm install --global @intflow/secflow-cli
16
16
  secflowctl --version
17
17
  ```
18
18
 
19
- The Python distribution requires Python 3.10+. Installation automatically includes the matching Admin-capable `secflow-sdk` dependency.
19
+ Python 3.10+ is required; installation includes the pinned Admin-capable SDK.
20
20
 
21
- The npm distribution includes Python and the CLI dependencies in OS-specific native packages. Initial targets: Windows x64, macOS Intel/Apple Silicon, and Linux x64 with glibc 2.35+ (Ubuntu 22.04+). Alpine/musl and Linux ARM are not npm targets yet; use the Python distribution there. Keep npm optional dependencies enabled. Installation has no postinstall script or runtime download. Native binaries are not platform-signed or notarized.
21
+ The npm distribution bundles Python and dependencies for Windows x64, macOS Intel/Apple Silicon, and Linux x64 glibc 2.35+. Alpine/musl and Linux ARM use the Python distribution. Keep optional dependencies enabled; installation has no postinstall script. Releases require all four same-version native packages. Binaries are not platform-signed or notarized.
22
22
 
23
- The bundled interpreter includes CA certificates for HTTPS. An explicit `SSL_CERT_FILE` or `SSL_CERT_DIR` takes precedence for organization-managed trust; TLS certificate verification stays enabled.
23
+ The bundle includes HTTPS CAs; `SSL_CERT_FILE`/`SSL_CERT_DIR` overrides trust without disabling TLS verification.
24
24
 
25
25
  ```sh
26
- pipx upgrade secflow-cli
27
- # Or use the same installer as the initial installation:
28
- python -m pip install --upgrade secflow-cli
29
- npm install --global @intflow/secflow-cli@latest
26
+ secflowctl update --check
27
+ secflowctl update --status
30
28
  ```
31
29
 
30
+ Commands check within 3s and stage protocol-1 stable runtimes in a private cache/venv, then run argv once; offline failures keep the last good runtime. TTY/JSON agree; help/version/check/status never install. `update --apply` installs; `--auto on|off` persists policy; `--pin VERSION` freezes an exact runtime; `--unpin` releases only policy. `INTFLOW_CLI_AUTO_UPDATE=0` stops automatic installs, allowing explicit apply/pin. Pins outrank off and fail closed. Internal native binaries update through npm.
31
+
32
32
  ## Connect
33
33
 
34
34
  ```sh
@@ -41,9 +41,9 @@ secflowctl --help
41
41
 
42
42
  `secflowctl` without arguments displays a quick start. `secflowctl login` opens Intflow login and creates the `production` profile for `https://secflow.intflow.dev` on first use. Existing profiles retain their selected server. For another server, first use `secflowctl profile add staging --base-url https://secflow.example.test`, then `secflowctl --profile staging login`. Global options precede the command. Every command supports `--help`, including examples. Account and role administration requires server authorization.
43
43
 
44
- `auth login` remains compatible with Slack-issued Admin sessions and prompts without echo for an existing session. Sessions are stored in Windows Credential Manager, macOS Keychain, or Linux Secret Service. Linux persistent login needs an unlocked Secret Service (for example GNOME Keyring) and a D-Bus session. If secure storage is unavailable, login fails without a plaintext file fallback. Installing the CLI does not create an account or grant access.
44
+ `auth login` prompts without echo for an existing Admin session token. Sessions are stored in Windows Credential Manager, macOS Keychain, or Linux Secret Service. Linux persistent login needs an unlocked Secret Service (for example GNOME Keyring) and a D-Bus session. If secure storage is unavailable, login fails without a plaintext file fallback. Installing the CLI does not create an account or grant access.
45
45
 
46
- When browser login is enabled on the server, run `secflowctl auth login --browser`. Sign in to Intflow, compare the code with your terminal, and approve the SecFlow login. Use `--browser --no-browser` to open the displayed URL manually. This requires an interactive terminal, an existing authorized SecFlow account, and OS credential storage. The CLI stores only the resulting SecFlow session. If login expires or the exchange fails, start a new login.
46
+ When browser login is enabled on the server, run `secflowctl auth login --browser`. Sign in to Intflow, compare the code with your terminal, and approve the SecFlow login. Use `--browser --no-browser` to open the displayed URL manually. This requires an interactive terminal, an existing authorized SecFlow account, and OS credential storage. The CLI stores only the resulting SecFlow session. A busy server is retried for a minute. If login expires or the exchange fails, log in again.
47
47
 
48
48
  For automation use `SECFLOW_ADMIN_SESSION_TOKEN` supplied by your secret manager, or `--token-stdin` with `--non-interactive`. Never put tokens in command arguments, profiles, shell history, or tracked files. SDK tokens cannot authorize these Admin commands. `auth logout` revokes the current server session; `auth forget` only removes the local credential.
49
49
 
@@ -114,6 +114,6 @@ For automation, use `--non-interactive` before the command and `--yes` on `put`.
114
114
 
115
115
  ## Credentials and values
116
116
 
117
- `tokens issue --save-as NAME` stores an SDK token separately from Admin profiles; `runtime ... --sdk-name NAME` uses it. `auth sessions issue --save-as PROFILE` creates a new destination profile. Both require a new destination name or explicit `--token-stdout` to redirected stdout; raw credentials never appear in ordinary JSON/table results. Linux/macOS non-interactive invocations require an explicitly injected credential rather than unlocking the OS store.
117
+ `tokens issue` uses the server-default lifetime; choose `--ttl-seconds` (1-7776000) or `--no-expiry`, never both. `--save-as NAME` stores an SDK token for `runtime ... --sdk-name NAME`. `auth sessions issue --save-as PROFILE` stores a separate Admin profile. Both need a new destination or `--token-stdout` redirected to a pipe/file; ordinary results contain no raw credentials. Linux/macOS non-interactive calls need an injected credential, not an OS store unlock.
118
118
 
119
- Secret create/rotate reads exact UTF-8 or base64 input through hidden input, `--value-stdin`, or `--value-file`. Do not put values in command arguments. Stdin cannot simultaneously carry an Admin token and a secret or policy document. Runtime commands use SDK credentials only; Admin sessions are never substituted. No plaintext resolve command is provided.
119
+ Secret create/rotate accepts exact UTF-8 or base64 via hidden input, `--value-stdin` or `--value-file`, never command arguments. Stdin cannot carry both an Admin token and a value or policy. Runtime uses SDK credentials, never Admin sessions; no plaintext resolve command exists.
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "secflow-cli"
7
- version = "0.2.4"
7
+ version = "0.3.0"
8
8
  description = "SecFlow operator CLI"
9
9
  readme = "USAGE.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.10"
12
+ keywords = ["intflow-cli-update-protocol-1"]
12
13
  dependencies = ["secflow-sdk==0.1.10", "keyring>=25,<26"]
13
14
 
14
15
  [project.scripts]
15
- secflowctl = "secflow_cli.main:main"
16
+ secflowctl = "secflow_cli.bootstrap:main"
16
17
 
17
18
  [project.urls]
18
19
  Repository = "https://github.com/yeon3724/secflow"
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,3 @@
1
+ from secflow_cli.bootstrap import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Any
5
+
6
+ from . import __version__
7
+ from .update_runtime import bootstrap
8
+ from .update_state import Configuration, default_root
9
+
10
+
11
+ def execute(arguments: list[str]) -> int:
12
+ from .main import main
13
+
14
+ return main(arguments, configure=True)
15
+
16
+
17
+ def output(value: dict[str, Any]) -> None:
18
+ from .output import update_result
19
+
20
+ update_result(value)
21
+
22
+
23
+ def main(argv: list[str] | None = None) -> int:
24
+ config = Configuration(
25
+ "secflow-cli", "secflow_cli", __version__, default_root("secflow-cli")
26
+ )
27
+ return bootstrap(
28
+ config,
29
+ list(sys.argv[1:] if argv is None else argv),
30
+ execute,
31
+ output,
32
+ frozen=bool(getattr(sys, "frozen", False)),
33
+ )
34
+
35
+
36
+ if __name__ == "__main__":
37
+ raise SystemExit(main())
@@ -4,6 +4,7 @@ import base64
4
4
  from datetime import datetime, timezone
5
5
  import hashlib
6
6
  import json
7
+ import logging
7
8
  import os
8
9
  import re
9
10
  import sys
@@ -11,12 +12,18 @@ import time
11
12
  from urllib.error import HTTPError
12
13
  from urllib.request import HTTPRedirectHandler, Request, build_opener
13
14
  from uuid import UUID
15
+
16
+
17
+ LOGGER = logging.getLogger(__name__)
14
18
  import webbrowser
15
19
 
16
20
  from secflow_sdk import SecFlowAdminClient
17
21
  from secflow_cli.errors import CliError
18
22
 
19
23
 
24
+ BUSY_WAIT_SECONDS = 60
25
+
26
+
20
27
  def rejected():
21
28
  return CliError(3, "BROWSER_LOGIN_FAILED", "Browser login did not complete. Start a new login; uncertain session exchanges are not retried.")
22
29
 
@@ -32,9 +39,9 @@ def post(url, body, timeout):
32
39
  try:
33
40
  response = build_opener(NoRedirect).open(request, timeout=timeout)
34
41
  except HTTPError as error:
35
- status = error.code
42
+ status, retry_after = error.code, _retry_after(error.headers.get("Retry-After"))
36
43
  error.close()
37
- return status, {}
44
+ return status, {}, retry_after
38
45
  with response:
39
46
  raw = response.read(16385)
40
47
  if len(raw) > 16384:
@@ -42,7 +49,13 @@ def post(url, body, timeout):
42
49
  value = json.loads(raw)
43
50
  if not isinstance(value, dict):
44
51
  raise rejected()
45
- return response.status, value
52
+ return response.status, value, None
53
+
54
+
55
+ def _retry_after(value):
56
+ if not isinstance(value, str) or not re.fullmatch(r"[0-9]{1,4}", value):
57
+ return None
58
+ return min(30, max(1, int(value)))
46
59
 
47
60
 
48
61
  def acquire_session(url, timeout, *, no_browser=False):
@@ -57,7 +70,8 @@ def acquire_session(url, timeout, *, no_browser=False):
57
70
  def _open_browser(verification_uri):
58
71
  try:
59
72
  opened = webbrowser.open(verification_uri)
60
- except Exception:
73
+ except Exception as caught:
74
+ LOGGER.debug("Browser could not be opened", extra={"attributes": {"error.type": type(caught).__name__}})
61
75
  opened = False
62
76
  if not opened:
63
77
  print("Open the URL above manually to continue.", file=sys.stderr)
@@ -75,10 +89,26 @@ def _issued_session(result):
75
89
  return {field: result[field] for field in ("session_token", "session_id", "account_id", "expires_at")}
76
90
 
77
91
 
92
+ def _create(url, proof, timeout):
93
+ give_up = time.monotonic() + BUSY_WAIT_SECONDS
94
+ announced = False
95
+ while True:
96
+ started_at = time.monotonic()
97
+ status, created, retry_after = post(url + "/v1/auth/browser-login", {"poll_challenge": hashlib.sha256(proof.encode()).hexdigest()}, timeout)
98
+ if status != 503 or retry_after is None:
99
+ return status, created, started_at
100
+ remaining = give_up - time.monotonic()
101
+ if remaining <= 0:
102
+ raise CliError(3, "BROWSER_LOGIN_BUSY", "The server is too busy to start a browser login. Try again in a few minutes.")
103
+ if not announced:
104
+ print("The server is busy. Retrying for up to " + str(BUSY_WAIT_SECONDS) + " seconds.", file=sys.stderr)
105
+ announced = True
106
+ time.sleep(min(retry_after, remaining))
107
+
108
+
78
109
  def _acquire_session(url, timeout, *, no_browser):
79
110
  proof = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode("ascii")
80
- started_at = time.monotonic()
81
- status, created = post(url + "/v1/auth/browser-login", {"poll_challenge": hashlib.sha256(proof.encode()).hexdigest()}, timeout)
111
+ status, created, started_at = _create(url, proof, timeout)
82
112
  if status == 404:
83
113
  raise CliError(3, "BROWSER_LOGIN_UNAVAILABLE", "This server has not enabled browser login. Use a Slack-issued Admin session.")
84
114
  if status != 201:
@@ -97,18 +127,26 @@ def _acquire_session(url, timeout, *, no_browser):
97
127
  print("Confirm code " + created["user_code"] + " and approve the login in your browser.", file=sys.stderr)
98
128
  if not no_browser:
99
129
  _open_browser(verification_uri)
100
- interval = created["interval"]
101
- while time.monotonic() + interval < deadline:
102
- time.sleep(interval)
130
+ return _poll(url, timeout, identifier, proof, deadline, created["interval"])
131
+
132
+
133
+ def _poll(url, timeout, identifier, proof, deadline, interval):
134
+ wait = interval
135
+ while time.monotonic() + wait < deadline:
136
+ time.sleep(wait)
137
+ wait = interval
103
138
  remaining = deadline - time.monotonic()
104
139
  if remaining <= 0:
105
140
  break
106
- status, result = post(url + "/v1/auth/browser-login/redeem",
107
- {"transaction_id": identifier, "poll_secret": proof}, min(timeout, remaining))
141
+ status, result, retry_after = post(url + "/v1/auth/browser-login/redeem",
142
+ {"transaction_id": identifier, "poll_secret": proof}, min(timeout, remaining))
108
143
  if status == 202 and result == {"status": "pending"}:
109
144
  continue
110
145
  if status == 429:
111
- interval = min(30, interval + 2)
146
+ wait = interval = min(30, interval + 2)
147
+ continue
148
+ if status == 503 and retry_after is not None:
149
+ wait = max(interval, retry_after)
112
150
  continue
113
151
  if status != 200:
114
152
  raise rejected()
@@ -128,7 +166,7 @@ def login(url, timeout, store, reference, *, no_browser=False):
128
166
  except BaseException:
129
167
  try:
130
168
  client.revoke_current_session()
131
- except Exception:
132
- pass
169
+ except Exception as caught:
170
+ LOGGER.debug("New session revocation failed", extra={"attributes": {"error.type": type(caught).__name__}})
133
171
  raise
134
172
  return account, session["expires_at"]