Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,16 @@ jobs:
with:
token: ${{ secrets.CI_SN3S_TOKEN }}

- name: Cache Poetry Dependencies
uses: actions/cache@v2
- name: Install uv
uses: astral-sh/setup-uv@v3

- name: Cache uv Dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
path: ~/.cache/uv
key: ${{ runner.os }}-uv-${{ hashFiles('**/uv.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
${{ runner.os }}-uv-

- name: Setup Git
run: |
Expand Down Expand Up @@ -129,7 +132,7 @@ jobs:
run: |
echo "Version: ${{ steps.semver.outputs.version }}"
docker build \
--build-arg PYTHON_IMAGE=${{ matrix.python_image }} \
--build-arg BASE_IMAGE=${{ matrix.python_image }} \
--build-arg BUILD_VERSION=${{ steps.calculate_version.outputs.version }} \
--tag my-image:${{ steps.semver.outputs.version }} \
.
Expand All @@ -138,15 +141,15 @@ jobs:
mkdir -p temp-dist
docker cp test-container:/app/dist temp-dist/
docker cp test-container:/app/pyproject.toml .
docker cp test-container:/app/poetry.lock .
docker cp test-container:/app/uv.lock .

- name: Commit and Push
if: matrix.python_image == env.default_image
run: |
git add reports/*
git add pyproject.toml
git add poetry.lock
git commit -m "AUTO: Update reports and poetry files from CI"
git add uv.lock
git commit -m "AUTO: Update reports and uv files from CI"
git push

- name: Publish to PyPi
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/pull_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ jobs:
echo "has_changes=false" >> $GITHUB_OUTPUT
fi

- name: Cache Poetry Dependencies
uses: actions/cache@v2
- name: Cache uv Dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
path: ~/.cache/uv
key: ${{ runner.os }}-uv-${{ hashFiles('**/uv.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
${{ runner.os }}-uv-

- name: Build and Test
if: steps.check_changes.outputs.has_changes == 'true'
run: |
docker build \
--build-arg PYTHON_IMAGE=${{ matrix.python_image }} \
--build-arg BASE_IMAGE=${{ matrix.python_image }} \
--build-arg BUILD_VERSION=${{ env.temp_build_version }} \
--target test \
--tag my-image:PR-${{ github.run_number }} \
Expand Down
39 changes: 0 additions & 39 deletions .github/workflows/sonarcloud.yaml

This file was deleted.

49 changes: 23 additions & 26 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,24 @@

WORKDIR /app

ENV POETRY_HOME="/opt/poetry" \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_VIRTUALENVS_IN_PROJECT=false \
POETRY_NO_INTERACTION=1
ENV PATH="$PATH:$POETRY_HOME/bin"

# Install dependencies
RUN apt-get update && apt-get install -y \
curl \
gcc \
make \
&& rm -rf /var/lib/apt/lists/*

# Install poetry
RUN curl -sSL https://install.python-poetry.org | python3 -

RUN poetry config virtualenvs.create false
# Install uv
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH"

###############################
# Install Dependencies #
###############################
FROM base AS dependencies

COPY pyproject.toml poetry.lock ./
RUN poetry install --no-root
COPY pyproject.toml uv.lock* ./
RUN uv sync --no-dev --no-install-project

###############################
# Build Image #
Expand All @@ -42,30 +35,34 @@

COPY . /app/

# Update version in pyproject.toml
RUN sed -i "s/^version = \".*\"/version = \"${BUILD_VERSION}\"/" pyproject.toml

Check warning on line 39 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=cesaregarza_SplatNet3_Scraper&issues=AZ09FvIdEKksgLqHP38F&open=AZ09FvIdEKksgLqHP38F&pullRequest=80

# Build the application
RUN poetry version $BUILD_VERSION && \
poetry build
RUN uv build

###############################
# Test Image #
###############################
FROM build AS test

RUN poetry install --extras "parquet" && \
poetry update
RUN uv sync --extra parquet --extra dev

Check warning on line 49 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=cesaregarza_SplatNet3_Scraper&issues=AZ09FvIdEKksgLqHP38G&open=AZ09FvIdEKksgLqHP38G&pullRequest=80

RUN J_PATH=reports/junit \
C_PATH=reports/coverage \
F_PATH=reports/flake8 && \
# Run tests with coverage
poetry run coverage run -m pytest --junitxml=$J_PATH/junit.xml --html=${J_PATH}/report.html -k . && \
poetry run coverage xml -o $C_PATH/coverage.xml --omit="app/tests/*" && \
poetry run coverage html -d $C_PATH/htmlcov --omit="app/tests/*" && \
uv run coverage run -m pytest --junitxml=$J_PATH/junit.xml --html=${J_PATH}/report.html -k . && \
uv run coverage xml -o $C_PATH/coverage.xml --omit="app/tests/*" && \
uv run coverage html -d $C_PATH/htmlcov --omit="app/tests/*" && \
# Generate badges
poetry run genbadge tests -o $J_PATH/test-badge.svg && \
poetry run genbadge coverage -o $C_PATH/coverage-badge.svg && \
# Mypy checks
poetry run mypy . && \
# Flake8 checks
poetry run flake8 src/ --format=html --htmldir ${F_PATH} --statistics --tee --output-file ${F_PATH}/flake8stats.txt && \
poetry run genbadge flake8 -i $F_PATH/flake8stats.txt -o $F_PATH/flake8-badge.svg
uv run genbadge tests -o $J_PATH/test-badge.svg && \
uv run genbadge coverage -o $C_PATH/coverage-badge.svg && \
# Type checks (pyright replaces mypy)
uv run pyright src/ && \
# Lint checks (ruff replaces flake8)
mkdir -p ${F_PATH} && \
uv run ruff check src/ --output-format concise --statistics > ${F_PATH}/flake8stats.txt || \
(cat ${F_PATH}/flake8stats.txt && exit 1) && \
cat ${F_PATH}/flake8stats.txt && \
uv run genbadge flake8 -i $F_PATH/flake8stats.txt -o $F_PATH/flake8-badge.svg
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,59 @@ The `query` module is an easy-to-use module that enables fast and painless query

The `query` module provides the `QueryHandler` class, which is used to make queries to the SplatNet 3 API. The `QueryHandler` class can be instantiated in one of a few ways: by providing a session token, by providing the path to a configuration file, or by loading environment variables.

### NXAPI authentication (2.0.0+)

Our default `f_token` provider now requires nxapi-auth client credentials. The default scope is `ca:gf ca:er ca:dr`, which matches the hosted nxapi examples and covers f-generation plus the optional encrypt/decrypt helpers. Choose **one** of the following client authentication methods:

- **Public client (no secret):** set only `NXAPI_ZNCA_API_CLIENT_ID`.
- **Confidential client:** set `NXAPI_ZNCA_API_CLIENT_ID` and `NXAPI_ZNCA_API_CLIENT_SECRET`.
- **Private key JWT:** either set a prebuilt `NXAPI_ZNCA_API_CLIENT_ASSERTION`, or
point the scraper at your PEM/JWKS metadata so it can mint fresh assertions
on demand.

Required environment variables:

```bash
export NXAPI_ZNCA_API_CLIENT_ID="your-client-id" # required
# choose ONE auth method
export NXAPI_ZNCA_API_CLIENT_SECRET="your-secret" # or
export NXAPI_ZNCA_API_CLIENT_ASSERTION="eyJhbGciOi..." # + NXAPI_ZNCA_API_CLIENT_ASSERTION_TYPE=...
# or generate private_key_jwt assertions locally
export NXAPI_ZNCA_API_CLIENT_ASSERTION_PRIVATE_KEY_PATH="/path/to/private.pem"
export NXAPI_ZNCA_API_CLIENT_ASSERTION_JKU="https://example.com/.well-known/jwks.json"
export NXAPI_ZNCA_API_CLIENT_ASSERTION_KID="my-key-id"

# optional but recommended for clarity
export NXAPI_ZNCA_API_AUTH_SCOPE="ca:gf ca:er ca:dr"
export NXAPI_USER_AGENT="my-scraper/2.0.0 (+https://example.com/contact)"
```

Library wiring example:

```ts
import { setClientAuthentication, addUserAgent } from 'nxapi';

addUserAgent('my-scraper/2.0.0 (+https://example.com/contact)');

setClientAuthentication({
id: process.env.NXAPI_ZNCA_API_CLIENT_ID,
// choose ONE of the following:
// secret: process.env.NXAPI_ZNCA_API_CLIENT_SECRET,
// assertion: process.env.NXAPI_ZNCA_API_CLIENT_ASSERTION,
// assertionType: process.env.NXAPI_ZNCA_API_CLIENT_ASSERTION_TYPE,
scope: 'ca:gf ca:er ca:dr',
});
```

If you store configuration in `.splatnet3_scraper`, `nxapi_client_secret` is the canonical key. The loader also accepts the legacy `nxapi_shared_secret` key when reading older files.
Generated assertions use `nxapi_client_assertion_private_key_path`,
`nxapi_client_assertion_jku`, and `nxapi_client_assertion_kid`.

> #### Using the hosted f-generation API?
> The default hosted `nxapi-znca-api` service receives a Nintendo `id_token` (JWT) to mint the required parameters. Review the Public API terms and status page linked in the upstream repository, and disclose this behaviour to your users.

The handler also tracks account cooldowns and token lifetimes. `401`/`403` responses trigger a token refresh followed by a short cooldown before the account is used again, while `429`/`503` responses schedule an exponential backoff with jitter and raise a `RateLimitException` so that orchestrators can rotate to another Nintendo account. Tokens are refreshed proactively based on the documented lifetimes (~15 minutes for the app `id_token`, ~2 hours for the web-service tokens) instead of minting new `f` values on every request.

### Using the `auth` module

:warning: **Warning: The `auth` module is intended for advanced users only. Most users should use the `scraper` or `query` modules for a simpler and more convenient experience.**
Expand Down Expand Up @@ -98,9 +151,21 @@ The following environment variables are supported:
- `SN3S_SESSION_TOKEN`
- `SN3S_GTOKEN`
- `SN3S_BULLET_TOKEN`
- `NXAPI_ZNCA_API_CLIENT_ID`
- `NXAPI_ZNCA_API_CLIENT_SECRET`
- `NXAPI_ZNCA_API_CLIENT_ASSERTION`
- `NXAPI_ZNCA_API_CLIENT_ASSERTION_PRIVATE_KEY_PATH`
- `NXAPI_ZNCA_API_CLIENT_ASSERTION_JKU`
- `NXAPI_ZNCA_API_CLIENT_ASSERTION_KID`
- `NXAPI_ZNCA_API_CLIENT_ASSERTION_TYPE`
- `NXAPI_ZNCA_API_AUTH_SCOPE`
- `NXAPI_USER_AGENT`
- `NXAPI_AUTH_TOKEN_URL`

The config file also accepts `nxapi_shared_secret` as a backwards-compatible alias for `nxapi_client_secret`.

```python
from splatnet3_scrape.query import QueryHandler
from splatnet3_scraper.query import QueryHandler
handler = QueryHandler.from_env()
handler.query("StageScheduleQuery")
```
Expand Down
16 changes: 16 additions & 0 deletions config/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Required
NXAPI_ZNCA_API_CLIENT_ID=CHANGE_ME

# Choose ONE authentication method
# NXAPI_ZNCA_API_CLIENT_SECRET=CHANGE_ME
# NXAPI_ZNCA_API_CLIENT_ASSERTION=eyJhbGciOi...
# NXAPI_ZNCA_API_CLIENT_ASSERTION_TYPE=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
# NXAPI_ZNCA_API_CLIENT_ASSERTION_PRIVATE_KEY_PATH=/path/to/private.pem
# NXAPI_ZNCA_API_CLIENT_ASSERTION_JKU=https://example.com/.well-known/jwks.json
# NXAPI_ZNCA_API_CLIENT_ASSERTION_KID=my-key-id

# Explicit scope matching the hosted nxapi examples
NXAPI_ZNCA_API_AUTH_SCOPE=ca:gf ca:er ca:dr

# Required by nxapi for non-Nintendo requests
NXAPI_USER_AGENT=my-scraper/1.2.0 (+https://example.com/contact)
38 changes: 38 additions & 0 deletions docs/UPGRADE-2.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Upgrade to 2.0.0 (NXAPI-auth)

## What changed

- The default f-token provider now requires nxapi-auth client credentials to call the hosted f-generation API.
- The default scope is `ca:gf ca:er ca:dr`, matching the hosted nxapi examples and covering the optional encrypt/decrypt helpers.

## Action required

1. Create an nxapi-auth client (a public client is sufficient if the portal does not expose secrets).
2. Set the following environment variables for each worker/process:

```bash
NXAPI_ZNCA_API_CLIENT_ID=... # required
# choose ONE
NXAPI_ZNCA_API_CLIENT_SECRET=... # or
NXAPI_ZNCA_API_CLIENT_ASSERTION=... # + NXAPI_ZNCA_API_CLIENT_ASSERTION_TYPE=...
# or generate private_key_jwt from your registered JWKS metadata
NXAPI_ZNCA_API_CLIENT_ASSERTION_PRIVATE_KEY_PATH=/path/to/private.pem
NXAPI_ZNCA_API_CLIENT_ASSERTION_JKU=https://example.com/.well-known/jwks.json
NXAPI_ZNCA_API_CLIENT_ASSERTION_KID=my-key-id
NXAPI_ZNCA_API_AUTH_SCOPE="ca:gf ca:er ca:dr"
NXAPI_USER_AGENT="my-scraper/2.0.0 (+https://example.com/contact)"
```

3. If you build from source or embed the library, call `setClientAuthentication({ id, scope: 'ca:gf ca:er ca:dr', ... })` before making requests.

## Notes

- The config loader still accepts `nxapi_shared_secret` when reading older config files, but `nxapi_client_secret` is the canonical key to write going forward.
- The hosted f-generation service has required client authentication since June 2025.
- If you use the hosted service, disclose that a Nintendo `id_token` is sent to a third party and link the Public API terms and status page.

## References

- nxapi README (environment variables, user-agent requirements, code examples).
- nxapi-znca-api README (public API terms, status page).
- nxapi-auth README (supported client authentication methods).
39 changes: 24 additions & 15 deletions docs/source/misc/login_flow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,29 @@ be used in a couple of steps later on to obtain different tokens.

In 2019, Nintendo made it more difficult for third party applications to obtain
a gtoken by introducing an `HMAC code <https://en.wikipedia.org/wiki/HMAC>`_.
The HMAC code is used to verify that the user is connecting through the
official Nintendo Switch Online app and not a third party application, like this
library. The HMAC code is generated through an obfuscated process that involves
the ID Token, the timestamp, and the request ID. The ID token is sent to a third
party server that is run by ``imink`` and returns the HMAC code (known as an "f"
token), a timestamp, and a request ID. The ID token, user data, "f" token,
timestamp, and request ID are then sent to the Nintendo Connect API to obtain a
Web API Server Credential Access Token from the Nintendo Connect API. The Web
API Server Credential Access Token is then sent to ``imink`` to generate another
HMAC code, timestamp, and request ID. This time, the Web API Server Credential
Access Token is sent to ``imink`` instead of the ID token, which returns a new
"f" token, timestamp, and request ID. The Web API Server Credential Access
Token, "f" token, timestamp, and request ID are then sent to the Nintendo
Connect API to obtain a gtoken.
The HMAC code (commonly called the ``f`` token) is used to verify that the user
is connecting through the official Nintendo Switch Online app and not a third
party application, like this library. The ``f`` token is generated through an
obfuscated process that involves the ID token, the timestamp, and the request
ID. ``splatnet3_scraper`` relies on a hosted implementation of this algorithm.

Starting with ``splatnet3_scraper`` ``2.0.0`` the default ``f_token`` provider is
`nxapi-znca-api <https://github.com/samuelthomas2774/nxapi-znca-api>`_. You must
register an ``nxapi-auth`` client (default scope ``ca:gf ca:er ca:dr``) and configure the corresponding
``NXAPI_ZNCA_API_CLIENT_*`` environment variables so the library can authenticate
with the hosted service. The :class:`NXAPIClient` automatically injects the
``Authorization`` header required by nxapi when it relays the ID token, request
ID, and timestamp. The environment variables are documented in
:ref:`Configuring NXAPI Client Authentication <nxapi_client_setup>`.

If the hosted service is unavailable, provide your own ``f_token_url`` list so
the library can contact an alternate implementation that you trust.

Putting this together: the ID token, user data, ``f`` token, timestamp, and
request ID are sent to Nintendo to obtain a Web API Server Credential Access
Token. That access token is in turn sent back to the ``f_token`` provider to
mint the phase-two values (``f`` token, timestamp, request ID). Sending those
values to Nintendo yields the gtoken.

Obtaining a Bullet Token
------------------------
Expand All @@ -129,4 +138,4 @@ requests are made using the bullet token obtained in the previous stage.

That's it for the login flow! If you have any questions or find any errors, feel
free to open an issue on the GitHub repository or open a pull request correcting
the issue.
the issue.
Loading
Loading