studylife-cli 1.8.2__tar.gz → 1.9.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.
- studylife_cli-1.9.0/.github/ISSUE_TEMPLATE/bug_report.yml +24 -0
- studylife_cli-1.9.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- studylife_cli-1.9.0/.github/ISSUE_TEMPLATE/feature_request.yml +16 -0
- studylife_cli-1.9.0/.github/PULL_REQUEST_TEMPLATE.md +9 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/.github/workflows/ci.yml +55 -9
- studylife_cli-1.9.0/.github/workflows/dependabot-auto-merge.yml +33 -0
- studylife_cli-1.9.0/.github/workflows/scorecard.yml +33 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/CHANGELOG.md +14 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/PKG-INFO +1 -1
- studylife_cli-1.9.0/SECURITY.md +13 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/src/studylife_cli/_version.py +2 -2
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/src/studylife_cli/login.py +27 -6
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/tests/test_login.py +12 -1
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/.github/dependabot.yml +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/.gitignore +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/.releaserc.json +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/LICENSE +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/README.md +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/pyproject.toml +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/src/studylife_cli/__init__.py +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/src/studylife_cli/cli.py +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/src/studylife_cli/client.py +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/src/studylife_cli/credentials.py +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/src/studylife_cli/models.py +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/src/studylife_cli/tui.py +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/tests/test_cli.py +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/tests/test_client.py +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/tests/test_tui.py +0 -0
- {studylife_cli-1.8.2 → studylife_cli-1.9.0}/uv.lock +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something does not work as documented
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what
|
|
7
|
+
attributes:
|
|
8
|
+
label: What happened?
|
|
9
|
+
description: What did you do, what did you expect, what happened instead?
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: input
|
|
13
|
+
id: version
|
|
14
|
+
attributes:
|
|
15
|
+
label: Version
|
|
16
|
+
description: Release or tag you are running (see the Releases page).
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: logs
|
|
21
|
+
attributes:
|
|
22
|
+
label: Logs or screenshots
|
|
23
|
+
description: Relevant log lines or a screenshot. Please remove tokens and personal data.
|
|
24
|
+
render: text
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest an improvement
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: What problem would this solve?
|
|
9
|
+
validations:
|
|
10
|
+
required: true
|
|
11
|
+
- type: textarea
|
|
12
|
+
id: proposal
|
|
13
|
+
attributes:
|
|
14
|
+
label: Proposed solution
|
|
15
|
+
validations:
|
|
16
|
+
required: true
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## What and why
|
|
2
|
+
|
|
3
|
+
<!-- One or two sentences: what changes, and why. Link the issue if there is one. -->
|
|
4
|
+
|
|
5
|
+
## Checklist
|
|
6
|
+
|
|
7
|
+
- [ ] Conventional Commit title (`fix:`, `feat:`, `ci:`, `docs:`, ...) - it decides whether a release is cut
|
|
8
|
+
- [ ] Tests or a manual verification note for behaviour changes
|
|
9
|
+
- [ ] No secrets, tokens or personal data in code, tests or logs
|
|
@@ -14,8 +14,8 @@ jobs:
|
|
|
14
14
|
lint:
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@v7.0.1
|
|
18
|
-
- uses: astral-sh/setup-uv@v10.0.1
|
|
17
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
18
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
19
19
|
with:
|
|
20
20
|
version: "0.5.8"
|
|
21
21
|
cache-suffix: "lint"
|
|
@@ -27,8 +27,8 @@ jobs:
|
|
|
27
27
|
test:
|
|
28
28
|
runs-on: ubuntu-latest
|
|
29
29
|
steps:
|
|
30
|
-
- uses: actions/checkout@v7.0.1
|
|
31
|
-
- uses: astral-sh/setup-uv@v10.0.1
|
|
30
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
31
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
32
32
|
with:
|
|
33
33
|
version: "0.5.8"
|
|
34
34
|
cache-suffix: "test"
|
|
@@ -50,9 +50,32 @@ jobs:
|
|
|
50
50
|
outputs:
|
|
51
51
|
published: ${{ steps.semrel.outputs.new_release_published }}
|
|
52
52
|
steps:
|
|
53
|
-
- uses: actions/checkout@v7.0.1
|
|
53
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
54
54
|
with:
|
|
55
55
|
fetch-depth: 0
|
|
56
|
+
# Ruleset bypass for the release commit (2026-09-11 main-protection rollout): main is
|
|
57
|
+
# protected by a ruleset (pull request + green test stage) that the GitHub Actions app
|
|
58
|
+
# cannot be a bypass actor for on a personal repository, so semantic-release pushes over
|
|
59
|
+
# SSH as a write-enabled DEPLOY KEY instead, which can. No-op while the secret is unset -
|
|
60
|
+
# the push then goes through the GITHUB_TOKEN https remote exactly as before. Needed in
|
|
61
|
+
# get-version too: @semantic-release/git's verifyConditions runs `git push --dry-run`
|
|
62
|
+
# even in dry-run mode. Same mechanism as lukislp/studylife's ci-cd.yml.
|
|
63
|
+
- name: Push as the semantic-release deploy key (if configured)
|
|
64
|
+
env:
|
|
65
|
+
DEPLOY_KEY: ${{ secrets.SEMANTIC_RELEASE_DEPLOY_KEY }}
|
|
66
|
+
run: |
|
|
67
|
+
if [ -z "$DEPLOY_KEY" ]; then
|
|
68
|
+
echo "SEMANTIC_RELEASE_DEPLOY_KEY is not set - semantic-release pushes with GITHUB_TOKEN"
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
72
|
+
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/semantic_release
|
|
73
|
+
chmod 600 ~/.ssh/semantic_release
|
|
74
|
+
# github.com's published ed25519 host key, pinned instead of ssh-keyscan'd at runtime:
|
|
75
|
+
# https://docs.github.com/en/authentication/keychain/githubs-ssh-key-fingerprints
|
|
76
|
+
echo "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" >> ~/.ssh/known_hosts
|
|
77
|
+
git remote set-url origin "git@github.com:${{ github.repository }}.git"
|
|
78
|
+
echo "GIT_SSH_COMMAND=ssh -i ~/.ssh/semantic_release -o IdentitiesOnly=yes" >> "$GITHUB_ENV"
|
|
56
79
|
- name: Sync to the current tip of main
|
|
57
80
|
run: |
|
|
58
81
|
git fetch origin main
|
|
@@ -80,9 +103,32 @@ jobs:
|
|
|
80
103
|
issues: write
|
|
81
104
|
pull-requests: write
|
|
82
105
|
steps:
|
|
83
|
-
- uses: actions/checkout@v7.0.1
|
|
106
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
84
107
|
with:
|
|
85
108
|
fetch-depth: 0
|
|
109
|
+
# Ruleset bypass for the release commit (2026-09-11 main-protection rollout): main is
|
|
110
|
+
# protected by a ruleset (pull request + green test stage) that the GitHub Actions app
|
|
111
|
+
# cannot be a bypass actor for on a personal repository, so semantic-release pushes over
|
|
112
|
+
# SSH as a write-enabled DEPLOY KEY instead, which can. No-op while the secret is unset -
|
|
113
|
+
# the push then goes through the GITHUB_TOKEN https remote exactly as before. Needed in
|
|
114
|
+
# get-version too: @semantic-release/git's verifyConditions runs `git push --dry-run`
|
|
115
|
+
# even in dry-run mode. Same mechanism as lukislp/studylife's ci-cd.yml.
|
|
116
|
+
- name: Push as the semantic-release deploy key (if configured)
|
|
117
|
+
env:
|
|
118
|
+
DEPLOY_KEY: ${{ secrets.SEMANTIC_RELEASE_DEPLOY_KEY }}
|
|
119
|
+
run: |
|
|
120
|
+
if [ -z "$DEPLOY_KEY" ]; then
|
|
121
|
+
echo "SEMANTIC_RELEASE_DEPLOY_KEY is not set - semantic-release pushes with GITHUB_TOKEN"
|
|
122
|
+
exit 0
|
|
123
|
+
fi
|
|
124
|
+
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
125
|
+
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/semantic_release
|
|
126
|
+
chmod 600 ~/.ssh/semantic_release
|
|
127
|
+
# github.com's published ed25519 host key, pinned instead of ssh-keyscan'd at runtime:
|
|
128
|
+
# https://docs.github.com/en/authentication/keychain/githubs-ssh-key-fingerprints
|
|
129
|
+
echo "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" >> ~/.ssh/known_hosts
|
|
130
|
+
git remote set-url origin "git@github.com:${{ github.repository }}.git"
|
|
131
|
+
echo "GIT_SSH_COMMAND=ssh -i ~/.ssh/semantic_release -o IdentitiesOnly=yes" >> "$GITHUB_ENV"
|
|
86
132
|
- id: semrel
|
|
87
133
|
uses: cycjimmy/semantic-release-action@b12c8f6015dc215fe37bc154d4ad456dd3833c90 # v6.0.0
|
|
88
134
|
with:
|
|
@@ -124,14 +170,14 @@ jobs:
|
|
|
124
170
|
permissions:
|
|
125
171
|
id-token: write # trusted publishing (OIDC) - no PyPI token/secret needed
|
|
126
172
|
steps:
|
|
127
|
-
- uses: actions/checkout@v7.0.1
|
|
173
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
128
174
|
with:
|
|
129
175
|
ref: main
|
|
130
176
|
fetch-depth: 0 # full history + tags, so hatch-vcs can find the tag just created
|
|
131
|
-
- uses: astral-sh/setup-uv@v10.0.1
|
|
177
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
132
178
|
with:
|
|
133
179
|
version: "0.5.8"
|
|
134
180
|
cache-suffix: "publish"
|
|
135
181
|
- run: uv python install 3.12
|
|
136
182
|
- run: uv build
|
|
137
|
-
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
183
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Dependabot auto-merge
|
|
2
|
+
|
|
3
|
+
# Merges Dependabot patch and minor updates once every required check is green; majors stay
|
|
4
|
+
# manual. Runs on pull_request_target so the base branch's workflow definition and the repo
|
|
5
|
+
# secrets apply (a Dependabot-triggered pull_request run gets neither). Merges with
|
|
6
|
+
# AUTOMERGE_TOKEN, a fine-grained PAT (contents + pull requests: write): a merge performed with
|
|
7
|
+
# GITHUB_TOKEN would never start the release workflow, because GitHub does not run workflows for
|
|
8
|
+
# events caused by GITHUB_TOKEN. Deliberately no checkout - PR code is never executed here.
|
|
9
|
+
on:
|
|
10
|
+
pull_request_target:
|
|
11
|
+
types: [opened, synchronize, reopened]
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
pull-requests: write
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
auto-merge:
|
|
19
|
+
if: github.actor == 'dependabot[bot]'
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
env:
|
|
22
|
+
HAS_TOKEN: ${{ secrets.AUTOMERGE_TOKEN != '' }}
|
|
23
|
+
steps:
|
|
24
|
+
- id: meta
|
|
25
|
+
uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0
|
|
26
|
+
with:
|
|
27
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
28
|
+
- if: env.HAS_TOKEN == 'true' && (steps.meta.outputs.update-type == 'version-update:semver-patch' || steps.meta.outputs.update-type == 'version-update:semver-minor')
|
|
29
|
+
env:
|
|
30
|
+
GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}
|
|
31
|
+
run: gh pr merge --auto --merge "${{ github.event.pull_request.html_url }}"
|
|
32
|
+
- if: env.HAS_TOKEN != 'true'
|
|
33
|
+
run: echo "AUTOMERGE_TOKEN is not set - leaving this PR for a manual merge"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: OpenSSF Scorecard
|
|
2
|
+
|
|
3
|
+
# Weekly supply-chain posture check (branch protection, pinned dependencies, token permissions,
|
|
4
|
+
# vulnerabilities, ...). Results land in the Security tab as code-scanning alerts and on
|
|
5
|
+
# https://securityscorecards.dev/viewer/?uri=github.com/lukislp/studylife-cli. Read-only, non-blocking.
|
|
6
|
+
on:
|
|
7
|
+
branch_protection_rule:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "17 4 * * 1"
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions: read-all
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
analysis:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
permissions:
|
|
20
|
+
security-events: write
|
|
21
|
+
id-token: write
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
24
|
+
with:
|
|
25
|
+
persist-credentials: false
|
|
26
|
+
- uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
|
|
27
|
+
with:
|
|
28
|
+
results_file: results.sarif
|
|
29
|
+
results_format: sarif
|
|
30
|
+
publish_results: true
|
|
31
|
+
- uses: github/codeql-action/upload-sarif@faaca9a8f6edddba5725ffe5adefdab6669a2eca # v3.38.0
|
|
32
|
+
with:
|
|
33
|
+
sarif_file: results.sarif
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.9.0](https://github.com/lukislp/studylife-cli/compare/v1.8.3...v1.9.0) (2026-09-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **login:** PKCE for the StudyLife connect round trip ([de702d8](https://github.com/lukislp/studylife-cli/commit/de702d8b4ecff987cc9e6901f1a30a93435a8629))
|
|
7
|
+
|
|
8
|
+
## [1.8.3](https://github.com/lukislp/studylife-cli/compare/v1.8.2...v1.8.3) (2026-09-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **ci:** push release commits as a deploy key so the default branch can be ruleset-protected ([deb7440](https://github.com/lukislp/studylife-cli/commit/deb7440b07cd58380deda44711e0bce342968c42))
|
|
14
|
+
|
|
1
15
|
## [1.8.2](https://github.com/lukislp/studylife-cli/compare/v1.8.1...v1.8.2) (2026-09-04)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: studylife-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Command-line client for StudyLife (self-hosted study organizer) - notes, sessions, course goals, and webhooks from your terminal.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report vulnerabilities privately through GitHub's private vulnerability reporting:
|
|
6
|
+
https://github.com/lukislp/studylife-cli/security/advisories/new
|
|
7
|
+
|
|
8
|
+
Do not open a public issue for security problems. You will get an acknowledgement within a few
|
|
9
|
+
days and a fix or mitigation plan before any public disclosure.
|
|
10
|
+
|
|
11
|
+
## Supported versions
|
|
12
|
+
|
|
13
|
+
Only the latest release receives security fixes.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.9.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 9, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -16,6 +16,8 @@ identity-contract-v1 §2 round trip this generalizes), with two real differences
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import base64
|
|
20
|
+
import hashlib
|
|
19
21
|
import hmac
|
|
20
22
|
import secrets
|
|
21
23
|
import sys
|
|
@@ -164,13 +166,22 @@ def run_login(
|
|
|
164
166
|
LoginError on any failure - callers decide how to present that (CLI prints and exits 1)."""
|
|
165
167
|
base_url = instance_url.rstrip("/")
|
|
166
168
|
state_token = secrets.token_urlsafe(32)
|
|
169
|
+
# PKCE (RFC 7636): the assertion comes back through a browser redirect, and StudyLife only
|
|
170
|
+
# redeems it together with the verifier matching this challenge - so a copy of the redirect
|
|
171
|
+
# URL alone (browser history, a proxy log) is worthless to anyone but this process.
|
|
172
|
+
code_verifier, code_challenge = _new_pkce_pair()
|
|
167
173
|
|
|
168
174
|
callback_server = _bind_first_free_port(candidate_ports)
|
|
169
175
|
redirect_uri = f"http://127.0.0.1:{callback_server.port}/callback"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
connect_query = urlencode(
|
|
177
|
+
{
|
|
178
|
+
"redirect_uri": redirect_uri,
|
|
179
|
+
"state": state_token,
|
|
180
|
+
"code_challenge": code_challenge,
|
|
181
|
+
"code_challenge_method": "S256",
|
|
182
|
+
}
|
|
173
183
|
)
|
|
184
|
+
connect_url = f"{base_url}/connect/client/{client_id}?{connect_query}"
|
|
174
185
|
|
|
175
186
|
print(f"Opening your browser to log in to StudyLife:\n {connect_url}")
|
|
176
187
|
print("Waiting for you to finish logging in and approving the connection...")
|
|
@@ -197,12 +208,22 @@ def run_login(
|
|
|
197
208
|
"been denied."
|
|
198
209
|
)
|
|
199
210
|
|
|
200
|
-
user_id, api_key = _exchange_assertion(base_url, client_id, result.assertion)
|
|
211
|
+
user_id, api_key = _exchange_assertion(base_url, client_id, result.assertion, code_verifier)
|
|
201
212
|
del user_id # not needed locally - kept for symmetry with the server's response shape
|
|
202
213
|
return Credentials(instance_url=base_url, client_id=client_id, api_key=api_key)
|
|
203
214
|
|
|
204
215
|
|
|
205
|
-
def
|
|
216
|
+
def _new_pkce_pair() -> tuple[str, str]:
|
|
217
|
+
"""(code_verifier, code_challenge): 43 unreserved characters and the base64url SHA-256 of
|
|
218
|
+
them without padding, exactly the S256 shape StudyLife's connect endpoint validates."""
|
|
219
|
+
verifier = secrets.token_urlsafe(32)
|
|
220
|
+
digest = hashlib.sha256(verifier.encode("ascii")).digest()
|
|
221
|
+
return verifier, base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _exchange_assertion(
|
|
225
|
+
base_url: str, client_id: str, assertion: str, code_verifier: str
|
|
226
|
+
) -> tuple[int, str]:
|
|
206
227
|
"""Server-to-server exchange of the single-use assertion for the user id and a freshly
|
|
207
228
|
issued, per-installation API key (generic flow - AuthController.10.OAuthClients.cs). No
|
|
208
229
|
X-Api-Key is sent: this endpoint is [AllowAnonymous] by design, the assertion itself is the
|
|
@@ -210,7 +231,7 @@ def _exchange_assertion(base_url: str, client_id: str, assertion: str) -> tuple[
|
|
|
210
231
|
try:
|
|
211
232
|
response = httpx.post(
|
|
212
233
|
f"{base_url}/api/auth/assertion-exchange",
|
|
213
|
-
json={"clientId": client_id, "assertion": assertion},
|
|
234
|
+
json={"clientId": client_id, "assertion": assertion, "codeVerifier": code_verifier},
|
|
214
235
|
timeout=10.0,
|
|
215
236
|
)
|
|
216
237
|
except httpx.HTTPError as exc:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
3
5
|
import threading
|
|
4
6
|
from urllib.parse import parse_qs, urlencode, urlparse
|
|
5
7
|
from urllib.request import urlopen
|
|
@@ -83,7 +85,16 @@ def test_run_login_completes_full_round_trip(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
83
85
|
return True
|
|
84
86
|
|
|
85
87
|
def fake_post(url: str, json: dict[str, object], timeout: float) -> httpx.Response:
|
|
86
|
-
assert
|
|
88
|
+
assert url == "https://studylife.example.com/api/auth/assertion-exchange"
|
|
89
|
+
assert json["clientId"] == "studylife-cli"
|
|
90
|
+
assert json["assertion"] == "the-assertion"
|
|
91
|
+
# PKCE: the verifier sent here must hash to the challenge the browser was sent with.
|
|
92
|
+
query = parse_qs(urlparse(opened_urls[0]).query)
|
|
93
|
+
assert query["code_challenge_method"] == ["S256"]
|
|
94
|
+
digest = hashlib.sha256(str(json["codeVerifier"]).encode("ascii")).digest()
|
|
95
|
+
assert query["code_challenge"] == [
|
|
96
|
+
base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
|
|
97
|
+
]
|
|
87
98
|
return httpx.Response(200, json={"userId": 1, "apiKey": "the-api-key"})
|
|
88
99
|
|
|
89
100
|
monkeypatch.setattr("studylife_cli.login.httpx.post", fake_post)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|