Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b89edfafa9 | ||
|
|
f1e5148480 | ||
|
|
1a28d120c2 | ||
|
|
e4e4337852 | ||
|
|
687c6b7cc6 | ||
|
|
20086b7776 | ||
|
|
29c63121f4 | ||
|
|
65018af7a0 | ||
|
|
d28b289048 | ||
|
|
4e8ca67ccc | ||
|
|
a0d54b426d | ||
|
|
5564a30bf9 | ||
|
|
2856d04396 | ||
|
|
868e56e5e8 | ||
|
|
2e386c1e63 | ||
|
|
39844e6e51 | ||
|
|
1e58ecd7f0 | ||
|
|
eb2be975c1 | ||
|
|
8c84e45bf9 | ||
|
|
c63806b094 | ||
|
|
06192f4717 | ||
|
|
5f09ad23ee | ||
|
|
5b42580245 | ||
|
|
b22ee5eeab | ||
|
|
c7e57aa015 | ||
|
|
2760f0234f | ||
|
|
d5a4027708 | ||
|
|
d555bc0727 |
@@ -0,0 +1,19 @@
|
||||
name: Bug report
|
||||
description: Report a reproducible defect.
|
||||
title: 'bug: '
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: List the smallest deterministic reproduction and expected behavior.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: verification
|
||||
attributes:
|
||||
label: Verification context
|
||||
description: Include repository, branch, version, and relevant checks. Never include secrets.
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Feature request
|
||||
description: Propose a change that is consistent with PLAN.md.
|
||||
title: 'feature: '
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem and proposed behavior
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: plan
|
||||
attributes:
|
||||
label: Planning
|
||||
options:
|
||||
- label: I checked PLAN.md and this does not silently change an existing decision.
|
||||
required: true
|
||||
@@ -9,4 +9,19 @@ jobs:
|
||||
runs-on: debian-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: test -f README.md && test -f AGENTS.md && test -f docs/versioning.md
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Validate repository documentation
|
||||
run: test -f README.md && test -f AGENTS.md && test -f docs/versioning.md
|
||||
- name: Validate JSON contracts and fixtures
|
||||
run: |
|
||||
node --input-type=module <<'NODE'
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
for (const directory of ['contracts/v1', 'fixtures/v1']) {
|
||||
for (const name of await readdir(directory)) {
|
||||
if (!name.endsWith('.json')) continue;
|
||||
JSON.parse(await readFile(`${directory}/${name}`, 'utf8'));
|
||||
}
|
||||
}
|
||||
NODE
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: API release check
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ['v*.*.*']
|
||||
|
||||
jobs:
|
||||
release-check:
|
||||
runs-on: debian-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Validate release metadata
|
||||
run: test -s CHANGELOG.md && test -s README.md && test -s docs/versioning.md
|
||||
- name: Validate JSON
|
||||
run: node --input-type=module -e "import { readdir, readFile } from 'node:fs/promises'; for (const d of ['contracts/v1','fixtures/v1']) for (const f of await readdir(d)) if (f.endsWith('.json')) JSON.parse(await readFile(d + '/' + f, 'utf8'));"
|
||||
@@ -0,0 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
All notable API contract changes are recorded here. The format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Development work is
|
||||
performed on `dev`; stable release entries are added when the user authorizes a
|
||||
release.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Add versioned backup inventory and manual-backup response contracts.
|
||||
@@ -4,5 +4,17 @@ Shared versioned HTTP and authenticated WebSocket contracts for `trixie-server`
|
||||
|
||||
This repository contains schemas, shared enums, error codes, compatibility fixtures, contract tests, and API documentation only.
|
||||
|
||||
The first contract slice is under `contracts/v1/` and `fixtures/v1/`. It covers server readiness and health so the client and server can be developed and tested together before Twitch integration.
|
||||
The first contract slice is under `contracts/v1/` and `fixtures/v1/`. It covers
|
||||
server readiness, health, errors, and the transport-neutral realtime event
|
||||
envelope so the client and server can be developed and tested together before
|
||||
Twitch integration. It also defines the first administrator authentication
|
||||
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.
|
||||
|
||||
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
|
||||
the authenticated `realtime-event.json` envelope; clients must ignore event
|
||||
types they do not understand.
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/activity-list-response.json",
|
||||
"title": "Trixie activity feed response",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["event_id", "event_type", "occurred_at", "payload"],
|
||||
"properties": {
|
||||
"event_id": { "type": "string", "minLength": 1 },
|
||||
"event_type": { "type": "string", "pattern": "^[a-z0-9_.-]+$" },
|
||||
"occurred_at": { "type": "string", "format": "date-time" },
|
||||
"payload": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"maxItems": 100
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "severity", "title", "message", "seen_at", "resolved_at", "created_at"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "format": "uuid" },
|
||||
"severity": { "type": "string", "enum": ["info", "warning", "error"] },
|
||||
"title": { "type": "string" },
|
||||
"message": { "type": "string" },
|
||||
"seen_at": { "type": ["string", "null"], "format": "date-time" },
|
||||
"resolved_at": { "type": ["string", "null"], "format": "date-time" },
|
||||
"created_at": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.invalid/contracts/v1/auth-login-request.json",
|
||||
"title": "Trixie Login Request",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["username", "password"],
|
||||
"properties": {
|
||||
"username": { "type": "string", "minLength": 1 },
|
||||
"password": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.invalid/contracts/v1/auth-login-response.json",
|
||||
"title": "Trixie Login Response",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["session_token", "must_replace_credentials"],
|
||||
"properties": {
|
||||
"session_token": { "type": "string", "minLength": 1 },
|
||||
"must_replace_credentials": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.invalid/contracts/v1/auth-session-response.json",
|
||||
"title": "Trixie Session Response",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["authenticated"],
|
||||
"properties": {
|
||||
"authenticated": { "const": true }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["filename", "size_bytes", "modified_at"],
|
||||
"properties": {
|
||||
"filename": { "type": "string" },
|
||||
"size_bytes": { "type": "integer", "minimum": 0 },
|
||||
"modified_at": { "type": ["string", "null"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["status", "filename", "reason"],
|
||||
"properties": {
|
||||
"status": { "type": "string", "enum": ["ok", "failed"] },
|
||||
"filename": { "type": ["string", "null"] },
|
||||
"reason": { "type": ["string", "null"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.invalid/contracts/v1/bot-command-invoke-request.json",
|
||||
"title": "Trixie bot command invocation request",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "user_id", "permission_level"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1, "maxLength": 64 },
|
||||
"user_id": { "type": "string", "minLength": 1, "maxLength": 64 },
|
||||
"permission_level": { "enum": ["everyone", "subscriber", "moderator", "broadcaster"] },
|
||||
"variables": { "type": "object", "additionalProperties": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.invalid/contracts/v1/bot-command-invoke-response.json",
|
||||
"title": "Trixie bot command invocation response",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["status", "response", "remaining_cooldown_seconds", "error"],
|
||||
"properties": {
|
||||
"status": { "enum": ["ok", "cooldown", "not_found", "permission_denied", "error"] },
|
||||
"response": { "type": ["string", "null"] },
|
||||
"remaining_cooldown_seconds": { "type": ["integer", "null"], "minimum": 0 },
|
||||
"error": { "type": ["string", "null"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/bot-commands-response.json",
|
||||
"title": "Trixie bot command list",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "description", "permission_level", "aliases", "tags", "cooldown_seconds", "cooldown_scope", "response_template", "error_template", "enabled", "failure_count", "disabled_reason"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "format": "uuid" },
|
||||
"name": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
||||
"description": { "type": "string" },
|
||||
"permission_level": { "enum": ["everyone", "subscriber", "moderator", "broadcaster"] },
|
||||
"aliases": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"cooldown_seconds": { "type": "integer", "minimum": 0, "maximum": 86400 },
|
||||
"cooldown_scope": { "enum": ["global", "user", "role"] },
|
||||
"response_template": { "type": "string" },
|
||||
"error_template": { "type": ["string", "null"] },
|
||||
"enabled": { "type": "boolean" },
|
||||
"failure_count": { "type": "integer", "minimum": 0 },
|
||||
"disabled_reason": { "type": ["string", "null"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/bot-secrets-response.json",
|
||||
"title": "Trixie bot secret metadata",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "description", "used_by", "created_at", "updated_at"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "format": "uuid" },
|
||||
"name": { "type": "string", "pattern": "^[A-Za-z0-9_]+$" },
|
||||
"description": { "type": "string" },
|
||||
"used_by": { "type": "array", "items": { "type": "string" } },
|
||||
"created_at": { "type": "string", "format": "date-time" },
|
||||
"updated_at": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"maxItems": 50,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "url", "provider"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"url": { "type": "string", "format": "uri", "pattern": "^https://" },
|
||||
"provider": { "type": "string", "enum": ["twitch", "7tv"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/chat-send-request.json",
|
||||
"title": "Twitch chat send request",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["identity", "message"],
|
||||
"properties": {
|
||||
"identity": { "enum": ["streamer", "bot"] },
|
||||
"message": { "type": "string", "minLength": 1, "maxLength": 500 },
|
||||
"reply_parent_message_id": { "type": "string", "maxLength": 128 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/chat-send-response.json",
|
||||
"title": "Twitch chat send response",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["message_id", "is_sent"],
|
||||
"properties": {
|
||||
"message_id": { "type": "string", "minLength": 1 },
|
||||
"is_sent": { "type": "boolean" },
|
||||
"drop_reason": { "type": ["string", "null"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "guests"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"guests": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["user_id"],
|
||||
"properties": {
|
||||
"user_id": { "type": "string" },
|
||||
"user_display_name": { "type": ["string", "null"] },
|
||||
"slot_id": { "type": ["string", "null"] },
|
||||
"is_live": { "type": ["boolean", "null"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "login", "display_name"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"login": { "type": "string" },
|
||||
"display_name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["twitch_user_id", "display_name", "favorite", "last_invited_at"],
|
||||
"properties": {
|
||||
"twitch_user_id": { "type": "string" },
|
||||
"display_name": { "type": "string" },
|
||||
"favorite": { "type": "boolean" },
|
||||
"last_invited_at": { "type": ["string", "null"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.invalid/contracts/v1/error.json",
|
||||
"title": "Trixie API Error",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["code", "message", "request_id"],
|
||||
"properties": {
|
||||
"code": { "type": "string", "pattern": "^[a-z0-9_]+$" },
|
||||
"message": { "type": "string", "minLength": 1 },
|
||||
"request_id": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/game-search-response.json",
|
||||
"title": "Twitch category search response",
|
||||
"type": "array",
|
||||
"maxItems": 25,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "box_art_url"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"box_art_url": { "type": "string", "format": "uri" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/moderation-action-request.json",
|
||||
"title": "Twitch moderation action",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["action", "user_id"],
|
||||
"properties": {
|
||||
"action": { "enum": ["ban", "timeout", "unban", "delete_message"] },
|
||||
"user_id": { "type": "string", "minLength": 1, "maxLength": 64 },
|
||||
"duration_seconds": { "type": "integer", "minimum": 1, "maximum": 1209600 },
|
||||
"reason": { "type": "string", "maxLength": 500 },
|
||||
"message_id": { "type": "string", "maxLength": 128 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/moderation-action-response.json",
|
||||
"title": "Twitch moderation action response",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["action", "applied"],
|
||||
"properties": {
|
||||
"action": { "type": "string" },
|
||||
"applied": { "const": true }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/overlay-response.json",
|
||||
"title": "Trixie public overlay",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["slug", "html", "css", "javascript", "enabled", "disabled_design"],
|
||||
"properties": {
|
||||
"slug": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
||||
"html": { "type": "string" },
|
||||
"css": { "type": "string" },
|
||||
"javascript": { "type": "string" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"disabled_design": { "type": "string" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.invalid/contracts/v1/permissions.json",
|
||||
"title": "Trixie Permission Level",
|
||||
"description": "The ordered permission levels used by chat commands and moderation controls.",
|
||||
"type": "string",
|
||||
"enum": ["everyone", "subscriber", "moderator", "broadcaster"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/readiness.json",
|
||||
"title": "Trixie readiness response",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["status"],
|
||||
"properties": {
|
||||
"status": { "enum": ["ready", "database_unavailable"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/realtime-auth-frame.json",
|
||||
"title": "Trixie realtime authentication frame",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["frame_type", "token"],
|
||||
"properties": {
|
||||
"frame_type": { "const": "authenticate" },
|
||||
"token": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.invalid/contracts/v1/realtime-event.json",
|
||||
"title": "Trixie Realtime Event",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["event_id", "event_type", "occurred_at", "payload"],
|
||||
"properties": {
|
||||
"event_id": { "type": "string", "minLength": 1 },
|
||||
"event_type": { "type": "string", "pattern": "^[a-z0-9_.-]+$" },
|
||||
"occurred_at": { "type": "string", "format": "date-time" },
|
||||
"payload": { "type": "object" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["status", "database", "twitch_identity_count", "unseen_notification_count", "realtime"],
|
||||
"properties": {
|
||||
"status": { "type": "string", "enum": ["healthy"] },
|
||||
"database": { "type": "string", "enum": ["connected"] },
|
||||
"twitch_identity_count": { "type": "integer", "minimum": 0 },
|
||||
"unseen_notification_count": { "type": "integer", "minimum": 0 },
|
||||
"realtime": { "type": "string", "enum": ["available"] }
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@
|
||||
"title": "Trixie Server Info",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["api_version", "server_version", "status"],
|
||||
"required": ["api_version", "server_version", "minimum_client_version", "maximum_client_version", "status"],
|
||||
"properties": {
|
||||
"api_version": { "type": "string", "const": "v1" },
|
||||
"server_version": { "type": "string", "minLength": 1 },
|
||||
"minimum_client_version": { "type": "string", "minLength": 1 },
|
||||
"maximum_client_version": { "type": "string", "minLength": 1 },
|
||||
"status": { "type": "string", "enum": ["ready"] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/shared-chat-session-response.json",
|
||||
"title": "Twitch Shared Chat session response",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"required": ["session_id", "host_broadcaster_id", "participants", "created_at", "updated_at"],
|
||||
"properties": {
|
||||
"session_id": { "type": "string", "minLength": 1 },
|
||||
"host_broadcaster_id": { "type": "string", "minLength": 1 },
|
||||
"participants": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["broadcaster_id"],
|
||||
"properties": { "broadcaster_id": { "type": "string", "minLength": 1 } }
|
||||
}
|
||||
},
|
||||
"created_at": { "type": "string", "format": "date-time" },
|
||||
"updated_at": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/stream-info-request.json",
|
||||
"title": "Twitch stream information update",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"title": { "type": "string", "maxLength": 140 },
|
||||
"game_id": { "type": "string", "maxLength": 64 },
|
||||
"language": { "type": "string", "maxLength": 10 },
|
||||
"tags": { "type": "array", "maxItems": 10, "items": { "type": "string", "maxLength": 25 } },
|
||||
"content_classification_labels": { "type": "array", "maxItems": 10, "items": { "type": "string", "maxLength": 64 } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/stream-info-response.json",
|
||||
"title": "Twitch stream information",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["broadcaster_id", "broadcaster_name", "game_id", "game_name", "title", "language", "tags", "content_classification_labels"],
|
||||
"properties": {
|
||||
"broadcaster_id": { "type": "string", "minLength": 1 },
|
||||
"broadcaster_name": { "type": "string", "minLength": 1 },
|
||||
"game_id": { "type": "string" },
|
||||
"game_name": { "type": "string" },
|
||||
"title": { "type": "string" },
|
||||
"language": { "type": "string" },
|
||||
"tags": { "type": "array", "items": { "type": "string" } },
|
||||
"content_classification_labels": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/twitch-oauth-link-response.json",
|
||||
"title": "Twitch OAuth authorization link response",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["identity", "scopes", "authorization_url", "expires_in_seconds"],
|
||||
"properties": {
|
||||
"identity": { "enum": ["streamer", "bot", "moderator"] },
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"authorization_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"pattern": "^https://id\\.twitch\\.tv/oauth2/authorize\\?"
|
||||
},
|
||||
"expires_in_seconds": { "type": "integer", "const": 600 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/twitch-oauth-result.json",
|
||||
"title": "Twitch OAuth completion response",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["identity", "twitch_user_id", "display_name"],
|
||||
"properties": {
|
||||
"identity": { "enum": ["streamer", "bot", "moderator"] },
|
||||
"twitch_user_id": { "type": "string", "minLength": 1 },
|
||||
"display_name": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.reaperofveriod.nl/contracts/v1/twitch-oauth-status-response.json",
|
||||
"title": "Twitch OAuth scope status response",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["identity", "connected", "required_scopes", "granted_scopes", "missing_scopes", "reauthorization_required"],
|
||||
"properties": {
|
||||
"identity": { "enum": ["streamer", "bot", "moderator"] },
|
||||
"connected": { "type": "boolean" },
|
||||
"required_scopes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"granted_scopes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"missing_scopes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"reauthorization_required": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://trixie.invalid/contracts/v1/version-negotiation.json",
|
||||
"title": "Trixie Version Negotiation",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["api_version", "client_version", "server_version", "compatible"],
|
||||
"properties": {
|
||||
"api_version": { "type": "string", "const": "v1" },
|
||||
"client_version": { "type": "string", "minLength": 1 },
|
||||
"server_version": { "type": "string", "minLength": 1 },
|
||||
"compatible": { "type": "boolean" },
|
||||
"reason": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,18 @@
|
||||
# Contract Testing
|
||||
|
||||
Fixtures cover HTTP responses, authenticated WebSocket events, errors, permissions, version negotiation, reconnects, and server-owned state transitions. Fixtures are deterministic, synthetic, and secret-free.
|
||||
Fixtures cover HTTP responses, authenticated WebSocket events, errors, permissions,
|
||||
version negotiation, reconnects, and server-owned state transitions. Fixtures are
|
||||
deterministic, synthetic, and secret-free. Every new contract must include at
|
||||
least one valid fixture and a negative case where validation rules are important.
|
||||
|
||||
The current realtime envelope is deliberately transport-neutral: the server may
|
||||
deliver it over an authenticated WebSocket or a future compatible transport, but
|
||||
the event identity and payload rules remain the same. `event_id` is the stable
|
||||
deduplication key for at-least-once delivery.
|
||||
|
||||
Browser clients authenticate the WebSocket with the first text frame described
|
||||
by `realtime-auth-frame.json`. Session tokens are sent in that frame rather
|
||||
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.
|
||||
|
||||
|
||||
@@ -5,4 +5,13 @@
|
||||
- Clients must tolerate unknown fields and events.
|
||||
- Breaking changes require a new major version, fixtures, migration notes, and coordinated releases.
|
||||
- Each release records minimum compatible client and server versions.
|
||||
- `GET /api/v1/server/info` publishes `minimum_client_version` and
|
||||
`maximum_client_version`; clients must use these fields to detect an
|
||||
incompatible pairing before enabling authenticated controls.
|
||||
- Permission values are shared through `contracts/v1/permissions.json`.
|
||||
They are ordered from least to most privileged: `everyone`, `subscriber`,
|
||||
`moderator`, and `broadcaster`.
|
||||
- The normalized result of a compatibility check is described by
|
||||
`contracts/v1/version-negotiation.json`; an incompatible result includes a
|
||||
human-readable `reason` while a compatible result may omit it.
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"event_id": "synthetic-event-1",
|
||||
"event_type": "channel.follow",
|
||||
"occurred_at": "2026-10-25T12:00:00Z",
|
||||
"payload": { "user_name": "SyntheticViewer" }
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000001",
|
||||
"severity": "warning",
|
||||
"title": "IP permanently rate-limited",
|
||||
"message": "A source IP reached the login failure limit and requires review.",
|
||||
"seen_at": null,
|
||||
"resolved_at": null,
|
||||
"created_at": "2026-10-25T12:00:00Z"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"session_token": "fixture-session-token-never-use",
|
||||
"must_replace_credentials": true
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"authenticated": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"filename": "trixie-20261025T030000Z.dump",
|
||||
"size_bytes": 4096,
|
||||
"modified_at": "2026-10-25T03:00:01Z"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": "ok",
|
||||
"filename": "trixie-20261025T030000Z.dump",
|
||||
"reason": null
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"status": "ok",
|
||||
"response": "Hello Alice",
|
||||
"remaining_cooldown_seconds": null,
|
||||
"error": null
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"id": "00000000-0000-4000-8000-000000000001",
|
||||
"name": "uptime",
|
||||
"description": "Show uptime",
|
||||
"permission_level": "everyone",
|
||||
"aliases": [],
|
||||
"tags": ["utility"],
|
||||
"cooldown_seconds": 2,
|
||||
"cooldown_scope": "user",
|
||||
"response_template": "Online",
|
||||
"error_template": null,
|
||||
"enabled": true,
|
||||
"failure_count": 0,
|
||||
"disabled_reason": null
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"id": "00000000-0000-4000-8000-000000000002",
|
||||
"name": "API_Key",
|
||||
"description": "External API key",
|
||||
"used_by": [],
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"name": "Kappa",
|
||||
"url": "https://static-cdn.jtvnw.net/emoticons/v2/25/static/dark/2.0",
|
||||
"provider": "twitch"
|
||||
},
|
||||
{
|
||||
"name": "catJAM",
|
||||
"url": "https://cdn.7tv.app/emote/60ae8f6f625f5a2a4d4e2b6f/2x.webp",
|
||||
"provider": "7tv"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"message_id": "synthetic-message-id",
|
||||
"is_sent": true,
|
||||
"drop_reason": null
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": "guest-star-session-1",
|
||||
"guests": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"id": "123456",
|
||||
"login": "example_streamer",
|
||||
"display_name": "ExampleStreamer"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"twitch_user_id": "123456",
|
||||
"display_name": "ExampleStreamer",
|
||||
"favorite": true,
|
||||
"last_invited_at": "2026-10-25T12:00:00Z"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "unauthorized",
|
||||
"message": "Authentication is required.",
|
||||
"request_id": "req_fixture_unauthorized"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"id": "123",
|
||||
"name": "Synthetic Game",
|
||||
"box_art_url": "https://static-cdn.jtvnw.net/ttv-boxart/123-52x72.jpg"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"action": "timeout",
|
||||
"applied": true
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"slug": "followers",
|
||||
"html": "<div id=\"alert\"></div>",
|
||||
"css": "#alert { color: white; }",
|
||||
"javascript": "",
|
||||
"enabled": true,
|
||||
"disabled_design": "transparent"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"status": "ready"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"frame_type": "authenticate",
|
||||
"token": "synthetic-session-token"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"event_id": "evt_fixture_activity_001",
|
||||
"event_type": "activity.follow",
|
||||
"occurred_at": "2026-01-01T00:00:00Z",
|
||||
"payload": {
|
||||
"username": "fixture_viewer",
|
||||
"display_name": "Fixture Viewer"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"status": "healthy",
|
||||
"database": "connected",
|
||||
"twitch_identity_count": 2,
|
||||
"unseen_notification_count": 0,
|
||||
"realtime": "available"
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"api_version": "v1",
|
||||
"server_version": "0.1.0-dev",
|
||||
"minimum_client_version": "0.1.0",
|
||||
"maximum_client_version": "0.x",
|
||||
"status": "ready"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"session_id": "shared-session-1",
|
||||
"host_broadcaster_id": "1234",
|
||||
"participants": [{ "broadcaster_id": "1234" }, { "broadcaster_id": "5678" }],
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"broadcaster_id": "synthetic-broadcaster-id",
|
||||
"broadcaster_name": "SyntheticStreamer",
|
||||
"game_id": "123",
|
||||
"game_name": "Synthetic Game",
|
||||
"title": "A synthetic stream",
|
||||
"language": "en",
|
||||
"tags": ["Synthetic"],
|
||||
"content_classification_labels": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"identity": "bot",
|
||||
"scopes": ["channel:bot", "user:read:chat", "user:write:chat"],
|
||||
"authorization_url": "https://id.twitch.tv/oauth2/authorize?client_id=public-example-client&redirect_uri=http%3A%2F%2Flocalhost%3A8787%2Fapi%2Fv1%2Ftwitch%2Foauth%2Fcallback&response_type=code&scope=channel%3Abot+user%3Aread%3Achat+user%3Awrite%3Achat&state=synthetic-state",
|
||||
"expires_in_seconds": 600
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"identity": "streamer",
|
||||
"twitch_user_id": "synthetic-twitch-user-id",
|
||||
"display_name": "SyntheticStreamer"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"identity": "streamer",
|
||||
"connected": false,
|
||||
"required_scopes": ["user:read:chat"],
|
||||
"granted_scopes": [],
|
||||
"missing_scopes": ["user:read:chat"],
|
||||
"reauthorization_required": true
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"api_version": "v1",
|
||||
"client_version": "0.1.0",
|
||||
"server_version": "0.1.0",
|
||||
"compatible": true
|
||||
}
|
||||
Reference in New Issue
Block a user