diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ad4761a..748c38d 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -14,6 +14,13 @@ jobs: node-version: 22 - name: Validate repository documentation run: test -f README.md && test -f AGENTS.md && test -f docs/versioning.md + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Validate restored-session schema and negative cases + run: | + python -m pip install jsonschema==4.26.0 + python tests/session_contract.py - name: Validate JSON contracts and fixtures run: | node --input-type=module <<'NODE' diff --git a/README.md b/README.md index 4dff980..132c4e1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ request and response envelopes; bearer values in fixtures are synthetic only. Twitch OAuth link and completion response envelopes are also versioned here; fixtures contain no real credentials or tokens. +`GET /api/v1/auth/session` returns `authenticated: true` and the current +`must_replace_credentials` flag from the administrator account. Restoring a +session must not bypass first-run setup. Older servers omit this flag; clients +must not interpret an absent flag as completed credential replacement. + The shared boundary vocabulary includes the ordered permission levels in `contracts/v1/permissions.json` and the client/server compatibility result in `contracts/v1/version-negotiation.json`. Server-owned state is delivered as @@ -49,4 +54,3 @@ only synthetic Twitch IDs and literal response templates. Typed command arguments use `bot-command-argument-definition.json` and remain literal validated data. - diff --git a/contracts/v1/auth-session-response.json b/contracts/v1/auth-session-response.json index 4f4ab2d..eea1273 100644 --- a/contracts/v1/auth-session-response.json +++ b/contracts/v1/auth-session-response.json @@ -4,8 +4,9 @@ "title": "Trixie Session Response", "type": "object", "additionalProperties": false, - "required": ["authenticated"], + "required": ["authenticated", "must_replace_credentials"], "properties": { - "authenticated": { "const": true } + "authenticated": { "const": true }, + "must_replace_credentials": { "type": "boolean" } } } diff --git a/docs/contract-testing.md b/docs/contract-testing.md index 9aa0033..38bedbf 100644 --- a/docs/contract-testing.md +++ b/docs/contract-testing.md @@ -16,3 +16,11 @@ than in a URL query parameter. The server closes the connection when the frame is missing, malformed, or invalid, and sends `connection.ready` only after successful validation. +The restored-session contract has Draft 2020-12 validation for both credential +replacement states, missing fields, wrong types, and unexpected properties: + +```sh +uv run --with jsonschema==4.26.0 python tests/session_contract.py +``` + +This isolated validation dependency is tooling only, not an application dependency. diff --git a/fixtures/v1/auth-session.authenticated.json b/fixtures/v1/auth-session.authenticated.json index aab331f..ad510d5 100644 --- a/fixtures/v1/auth-session.authenticated.json +++ b/fixtures/v1/auth-session.authenticated.json @@ -1,3 +1,4 @@ { - "authenticated": true + "authenticated": true, + "must_replace_credentials": false } diff --git a/fixtures/v1/auth-session.bootstrap.json b/fixtures/v1/auth-session.bootstrap.json new file mode 100644 index 0000000..e901e5c --- /dev/null +++ b/fixtures/v1/auth-session.bootstrap.json @@ -0,0 +1,4 @@ +{ + "authenticated": true, + "must_replace_credentials": true +} diff --git a/tests/session_contract.py b/tests/session_contract.py new file mode 100644 index 0000000..023fa98 --- /dev/null +++ b/tests/session_contract.py @@ -0,0 +1,24 @@ +"""Validate restored-session fixtures and rejection cases against Draft 2020-12.""" + +import json +from pathlib import Path + +from jsonschema import Draft202012Validator + +root = Path(__file__).resolve().parents[1] +schema = json.loads((root / "contracts/v1/auth-session-response.json").read_text()) +Draft202012Validator.check_schema(schema) +validator = Draft202012Validator(schema) + +for name in ["auth-session.authenticated.json", "auth-session.bootstrap.json"]: + validator.validate(json.loads((root / "fixtures/v1" / name).read_text())) + +for invalid in [ + {"authenticated": True}, + {"authenticated": False, "must_replace_credentials": False}, + {"authenticated": True, "must_replace_credentials": "false"}, + {"authenticated": True, "must_replace_credentials": False, "unexpected": True}, +]: + assert not validator.is_valid(invalid), "Invalid session response was accepted" + +print("Session schema: two valid fixtures and four rejection cases passed")