From 47290d2d8fdc882a44430a7a9bd0b59cbbafc603 Mon Sep 17 00:00:00 2001 From: stephan Date: Wed, 31 Dec 2025 00:05:46 +0100 Subject: [PATCH] feat: implement update-service v1 migration and enrollment flow - added /update/enroll endpoint and enrollment logic - migrated update client to v1 api endpoints and bearer auth - implemented remote status reporting in backend and scripts - updated requirements and project status --- backend/app.py | 20 +++ backend/models.py | 10 ++ backend/settings.py | 20 ++- backend/update.py | 94 +++++++++++- docs/admin-token-operations.md | 91 ++++++++++++ docs/architecture/openapi.yaml | 57 ++++++++ .../paths/admin-enrollment-tokens-export.yaml | 31 ++++ .../paths/admin-enrollment-tokens-revoke.yaml | 30 ++++ .../paths/admin-enrollment-tokens.yaml | 69 +++++++++ docs/architecture/openapi/paths/artifact.yaml | 58 ++++++++ docs/architecture/openapi/paths/enroll.yaml | 65 +++++++++ docs/architecture/openapi/paths/limits.yaml | 48 +++++++ docs/architecture/openapi/paths/manifest.yaml | 47 ++++++ docs/architecture/openapi/paths/releases.yaml | 126 ++++++++++++++++ docs/architecture/openapi/paths/status.yaml | 84 +++++++++++ .../openapi/schemas/enroll-request.yaml | 20 +++ .../openapi/schemas/enroll-response.yaml | 19 +++ .../enrollment-token-create-request.yaml | 20 +++ .../enrollment-token-create-response.yaml | 11 ++ .../openapi/schemas/enrollment-token.yaml | 39 +++++ docs/architecture/openapi/schemas/error.yaml | 15 ++ .../openapi/schemas/limits-policy.yaml | 10 ++ docs/architecture/openapi/schemas/limits.yaml | 39 +++++ .../openapi/schemas/manifest.yaml | 24 ++++ .../openapi/schemas/status-report.yaml | 42 ++++++ .../openapi/schemas/upload-response.yaml | 16 +++ docs/client-quickstart.md | 44 ++++++ docs/third-party-api.md | 135 ++++++++++++++++++ project-management/PROJECT_STATUS.md | 6 + project-management/feedback/teal/app_icon.svg | 10 ++ .../feedback/teal/app_icon_ai.svg | 9 ++ project-management/feedback/teal/favicon.svg | 7 + project-management/feedback/teal/logo.svg | 15 ++ project-management/feedback/teal/logo_ai.svg | 16 +++ .../requirements/epics/EPIC_000010.md | 2 +- .../requirements/stories/US_000034.md | 2 +- .../requirements/stories/US_000035.md | 2 +- .../requirements/tasks/TASK_000040.md | 2 +- .../requirements/tasks/TASK_000041.md | 2 +- scripts/rollback_client.sh | 24 ++++ scripts/update_client.sh | 22 ++- 41 files changed, 1384 insertions(+), 19 deletions(-) create mode 100644 docs/admin-token-operations.md create mode 100644 docs/architecture/openapi.yaml create mode 100644 docs/architecture/openapi/paths/admin-enrollment-tokens-export.yaml create mode 100644 docs/architecture/openapi/paths/admin-enrollment-tokens-revoke.yaml create mode 100644 docs/architecture/openapi/paths/admin-enrollment-tokens.yaml create mode 100644 docs/architecture/openapi/paths/artifact.yaml create mode 100644 docs/architecture/openapi/paths/enroll.yaml create mode 100644 docs/architecture/openapi/paths/limits.yaml create mode 100644 docs/architecture/openapi/paths/manifest.yaml create mode 100644 docs/architecture/openapi/paths/releases.yaml create mode 100644 docs/architecture/openapi/paths/status.yaml create mode 100644 docs/architecture/openapi/schemas/enroll-request.yaml create mode 100644 docs/architecture/openapi/schemas/enroll-response.yaml create mode 100644 docs/architecture/openapi/schemas/enrollment-token-create-request.yaml create mode 100644 docs/architecture/openapi/schemas/enrollment-token-create-response.yaml create mode 100644 docs/architecture/openapi/schemas/enrollment-token.yaml create mode 100644 docs/architecture/openapi/schemas/error.yaml create mode 100644 docs/architecture/openapi/schemas/limits-policy.yaml create mode 100644 docs/architecture/openapi/schemas/limits.yaml create mode 100644 docs/architecture/openapi/schemas/manifest.yaml create mode 100644 docs/architecture/openapi/schemas/status-report.yaml create mode 100644 docs/architecture/openapi/schemas/upload-response.yaml create mode 100644 docs/client-quickstart.md create mode 100644 docs/third-party-api.md create mode 100644 project-management/feedback/teal/app_icon.svg create mode 100644 project-management/feedback/teal/app_icon_ai.svg create mode 100644 project-management/feedback/teal/favicon.svg create mode 100644 project-management/feedback/teal/logo.svg create mode 100644 project-management/feedback/teal/logo_ai.svg diff --git a/backend/app.py b/backend/app.py index 4cf6f33..3a83e1e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -19,6 +19,8 @@ from backend.auth import ( from backend.models import ( ActionRequest, ActionResponse, + EnrollRequest, + EnrollResponse, LoginRequest, LoginResponse, UpdateActionResponse, @@ -239,6 +241,24 @@ def update_status(settings: Settings = Depends(get_settings)) -> UpdateStatus: return UpdateStatus(**status_data) +@app.post("/update/enroll", response_model=EnrollResponse, dependencies=[Depends(get_current_admin)]) +def update_enroll( + payload: EnrollRequest | None = Body(default=None), + settings: Settings = Depends(get_settings), +) -> EnrollResponse: + if payload and payload.enroll_token: + settings.update_enroll_token = payload.enroll_token + try: + update.enroll(settings) + return EnrollResponse(enrolled=True, message="Enrollment successful") + except Exception as exc: + logger.exception("Enrollment failed") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Enrollment failed: {str(exc)}", + ) from exc + + @app.post("/update/check", response_model=UpdateCheckResponse, dependencies=[Depends(get_current_admin)]) def update_check(settings: Settings = Depends(get_settings)) -> UpdateCheckResponse: try: diff --git a/backend/models.py b/backend/models.py index d6f83ca..cf0de50 100644 --- a/backend/models.py +++ b/backend/models.py @@ -41,6 +41,16 @@ class UpdateStatus(BaseModel): last_status: str last_error: Optional[str] = None last_timestamp: Optional[str] = None + enrolled: bool = False + + +class EnrollRequest(BaseModel): + enroll_token: Optional[str] = None + + +class EnrollResponse(BaseModel): + enrolled: bool + message: str class UpdateCheckResponse(BaseModel): diff --git a/backend/settings.py b/backend/settings.py index 8d7663a..48a1088 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -40,7 +40,13 @@ class Settings: self.notify_timeout: int = int(os.getenv("SKD_NOTIFY_TIMEOUT", "5")) self.dry_run: bool = os.getenv("SKD_DRY_RUN", "false").lower() == "true" self.update_url: str = os.getenv("SKD_UPDATE_URL", "https://update.wlkns.org") - self.update_token: str = os.getenv("SKD_UPDATE_TOKEN", "") + self.update_service_url: str = os.getenv("SKD_UPDATE_SERVICE_URL", "https://update.wlkns.org") + self.update_project_id: str = os.getenv("SKD_UPDATE_PROJECT_ID", "safe-kiddo-control") + self.update_enroll_token: str = os.getenv("SKD_UPDATE_ENROLL_TOKEN", "") + self.update_token_file: str = os.getenv( + "SKD_UPDATE_TOKEN_FILE", "/var/lib/skd/update_token" + ) + self.update_token: str = self._load_update_token() self.update_interval: int = int(os.getenv("SKD_UPDATE_INTERVAL", "3600")) self.update_status_url: str = os.getenv( "SKD_UPDATE_STATUS_URL", "https://update.wlkns.org/status" @@ -63,6 +69,18 @@ class Settings: def _parse_list(value: str) -> List[str]: return [item for item in (part.strip() for part in value.split(",")) if item] + def _load_update_token(self) -> str: + env_token = os.getenv("SKD_UPDATE_TOKEN", "") + if env_token: + return env_token + if os.path.exists(self.update_token_file): + try: + with open(self.update_token_file, "r", encoding="utf-8") as f: + return f.read().strip() + except OSError: + pass + return "" + @lru_cache(maxsize=1) def get_settings() -> Settings: diff --git a/backend/update.py b/backend/update.py index 8629dc5..eb9acdb 100644 --- a/backend/update.py +++ b/backend/update.py @@ -70,20 +70,56 @@ def get_status(settings: Settings) -> Dict[str, Any]: "last_status": data.get("status", "unknown"), "last_error": data.get("error") or None, "last_timestamp": data.get("timestamp"), + "enrolled": bool(settings.update_token), } +def enroll(settings: Settings) -> str: + if not settings.update_enroll_token: + raise ValueError("No enrollment token provided in settings") + + enroll_url = f"{settings.update_service_url}/v1/enroll" + payload = { + "project_id": settings.update_project_id, + "client_id": os.uname().nodename, + "software_id": "safe-kiddo", + "enroll_token": settings.update_enroll_token, + } + + with httpx.Client(timeout=10.0) as client: + response = client.post(enroll_url, json=payload) + response.raise_for_status() + data = response.json() + + token = data.get("token") + if not token: + raise ValueError("Enrollment response did not contain a token") + + # Save token + token_path = Path(settings.update_token_file) + _ensure_parent(token_path) + token_path.write_text(token, encoding="utf-8") + # Update settings object for immediate use + settings.update_token = token + + return token + + def _parse_version(value: str) -> List[int]: return [int(part) for part in value.split(".")] def check_update(settings: Settings) -> Dict[str, Any]: - headers = {} - if settings.update_token: - headers["Authorization"] = f"Bearer {settings.update_token}" + if not settings.update_token: + raise ValueError("Client is not enrolled (missing update token)") + + headers = {"Authorization": f"Bearer {settings.update_token}"} + manifest_url = ( + f"{settings.update_service_url}/v1/projects/{settings.update_project_id}/manifest" + ) with httpx.Client(timeout=10.0) as client: - response = client.get(settings.update_url, headers=headers) + response = client.get(manifest_url, headers=headers) response.raise_for_status() manifest = response.json() @@ -109,21 +145,65 @@ def check_update(settings: Settings) -> Dict[str, Any]: } +def report_status( + settings: Settings, + status: str, + version: str, + error: str | None = None, + duration_ms: int | None = None, +) -> None: + if not settings.update_token: + return + + report_url = ( + f"{settings.update_service_url}/v1/projects/{settings.update_project_id}/status" + ) + payload = { + "project_id": settings.update_project_id, + "version": version, + "status": status, + "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "client_id": os.uname().nodename, + } + if error: + payload["error_code"] = error + payload["reason"] = error + if duration_ms is not None: + payload["duration_ms"] = duration_ms + + try: + headers = {"Authorization": f"Bearer {settings.update_token}"} + with httpx.Client(timeout=10.0) as client: + client.post(report_url, json=payload, headers=headers).raise_for_status() + except Exception: + # We don't want to crash if status reporting fails + pass + + def _run_async(script_path: Path, settings: Settings) -> None: env = os.environ.copy() + env["SKD_UPDATE_SERVICE_URL"] = settings.update_service_url + env["SKD_UPDATE_PROJECT_ID"] = settings.update_project_id + env["SKD_UPDATE_TOKEN"] = settings.update_token env["SKD_UPDATE_STATUS_FILE"] = settings.update_status_file env["SKD_UPDATE_LOG_FILE"] = settings.update_log_file - subprocess.Popen([str(script_path)], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.Popen( + [str(script_path)], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) def start_update(settings: Settings, version: str | None = None) -> None: - _write_status(settings, "in_progress", version or _read_version()) + current_version = version or _read_version() + _write_status(settings, "in_progress", current_version) + report_status(settings, "in_progress", current_version) script = _project_root() / "scripts" / "update_client.sh" _run_async(script, settings) def start_rollback(settings: Settings) -> None: - _write_status(settings, "in_progress", _read_version()) + current_version = _read_version() + _write_status(settings, "in_progress", current_version) + report_status(settings, "in_progress", current_version) script = _project_root() / "scripts" / "rollback_client.sh" _run_async(script, settings) diff --git a/docs/admin-token-operations.md b/docs/admin-token-operations.md new file mode 100644 index 0000000..e0e4744 --- /dev/null +++ b/docs/admin-token-operations.md @@ -0,0 +1,91 @@ +ID: DOC_000006 | Version: 0.1.0 | Status: Draft + +# Admin Token Operations + +## Purpose +This document describes how operators create and manage pre-shared enrollment tokens for clients. + +## Pre-Shared Token Creation +Operators generate a single-use enrollment token and share it out-of-band with the client. + +Recommended properties: +- Single-use only +- Short TTL (e.g., 24h) +- Scoped to `project_id` and optional `client_id`/`software_id` + +## Admin Interfaces +We provide both an Admin API and a CLI tool for token operations. A frontend will be added later. + +### Admin User and Access +- An admin user must exist to operate token workflows. +- Initial access uses a local admin token. +- Later, admin auth will be integrated with the OIDC service. + +### CLI and Admin API Capabilities +- Create enrollment tokens +- List token metadata (no plaintext output) +- Revoke tokens +- Export a token as a file for client installation + +### Local Admin Token (Initial Phase) +- Admin requests must include `Authorization: Bearer `. +- The admin token is stored locally (e.g., `.env`) and never committed. + +Example `.env` (local only): +``` +ADMIN_TOKEN=change-me-please +``` + +Minimal flow (first token): +1) Set `ADMIN_TOKEN` in `.env`. +2) Call `POST /v1/admin/enrollment-tokens` with the bearer token. +3) Export the returned one-time token to a file and hand it to the client. + +## Admin API (Draft) +All admin endpoints are authenticated. Initial auth is local; later OIDC. + +Base path: +- `/v1/admin` + +Endpoints: +- `POST /v1/admin/enrollment-tokens` + - Create a pre-shared enrollment token. + - Request: `project_id`, optional `client_id`, optional `software_id`, optional `expires_at`. + - Response: token metadata + one-time plaintext token. +- `GET /v1/admin/enrollment-tokens` + - List token metadata (never return plaintext tokens). + - Supports filtering by `project_id`, `client_id`, `status` (active/used/expired). +- `POST /v1/admin/enrollment-tokens/{token_id}/revoke` + - Revoke a token (marks as revoked or sets `used_at`/`revoked_at`). +- `GET /v1/admin/enrollment-tokens/{token_id}/export` + - Export the one-time token to a file download (single use). + +## CLI (Draft) +Example commands (names can be adjusted): +- `update-service admin token create --project [--client ] [--software ] [--expires ]` +- `update-service admin token list --project [--status active|used|expired|revoked]` +- `update-service admin token revoke --id ` +- `update-service admin token export --id --out ./enroll-token.txt` + +Example format: +``` +enroll_ +``` + +## Storage and Safety +- Store only a hash of the enrollment token (never plaintext). +- Track `created_at`, `expires_at`, and `used_at`. +- Deny enrollment if `expires_at` is exceeded or `used_at` is set. + +## Rotation and Revocation +- Revoke enrollment tokens by invalidating their stored hash. +- Issue a new enrollment token if the previous one expires or is leaked. + +## Distribution +Preferred channels: +- One-time install code (copy/paste) +- QR code +- Encrypted file included in an install bundle + +## Audit Expectations +- Log token creation and enrollment usage for traceability. diff --git a/docs/architecture/openapi.yaml b/docs/architecture/openapi.yaml new file mode 100644 index 0000000..6026300 --- /dev/null +++ b/docs/architecture/openapi.yaml @@ -0,0 +1,57 @@ +openapi: 3.0.3 +info: + title: Update Webservice API + version: 0.1.0 +servers: + - url: https://update.wlkns.org + - url: https://staging.update.wlkns.org +security: + - bearerAuth: [] +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + Manifest: + $ref: './openapi/schemas/manifest.yaml' + StatusReport: + $ref: './openapi/schemas/status-report.yaml' + UploadResponse: + $ref: './openapi/schemas/upload-response.yaml' + EnrollRequest: + $ref: './openapi/schemas/enroll-request.yaml' + EnrollResponse: + $ref: './openapi/schemas/enroll-response.yaml' + EnrollmentToken: + $ref: './openapi/schemas/enrollment-token.yaml' + EnrollmentTokenCreateRequest: + $ref: './openapi/schemas/enrollment-token-create-request.yaml' + EnrollmentTokenCreateResponse: + $ref: './openapi/schemas/enrollment-token-create-response.yaml' + Error: + $ref: './openapi/schemas/error.yaml' + Limits: + $ref: './openapi/schemas/limits.yaml' + LimitsPolicy: + $ref: './openapi/schemas/limits-policy.yaml' +paths: + /v1/enroll: + $ref: './openapi/paths/enroll.yaml' + /v1/admin/enrollment-tokens: + $ref: './openapi/paths/admin-enrollment-tokens.yaml' + /v1/admin/enrollment-tokens/{token_id}/revoke: + $ref: './openapi/paths/admin-enrollment-tokens-revoke.yaml' + /v1/admin/enrollment-tokens/{token_id}/export: + $ref: './openapi/paths/admin-enrollment-tokens-export.yaml' + /v1/projects/{project_id}/manifest: + $ref: './openapi/paths/manifest.yaml' + /v1/projects/{project_id}/releases/{version}/artifact: + $ref: './openapi/paths/artifact.yaml' + /v1/projects/{project_id}/status: + $ref: './openapi/paths/status.yaml' + /v1/projects/{project_id}/releases: + $ref: './openapi/paths/releases.yaml' + /v1/limits: + $ref: './openapi/paths/limits.yaml' diff --git a/docs/architecture/openapi/paths/admin-enrollment-tokens-export.yaml b/docs/architecture/openapi/paths/admin-enrollment-tokens-export.yaml new file mode 100644 index 0000000..8c14399 --- /dev/null +++ b/docs/architecture/openapi/paths/admin-enrollment-tokens-export.yaml @@ -0,0 +1,31 @@ +get: + summary: Export enrollment token + x-auth-scopes: [admin] + parameters: + - name: token_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Token file + content: + text/plain: + schema: + type: string + example: enroll_6f3d2c... + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + '404': + description: Not Found + x-error-codes: [not_found] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' diff --git a/docs/architecture/openapi/paths/admin-enrollment-tokens-revoke.yaml b/docs/architecture/openapi/paths/admin-enrollment-tokens-revoke.yaml new file mode 100644 index 0000000..b08f4b0 --- /dev/null +++ b/docs/architecture/openapi/paths/admin-enrollment-tokens-revoke.yaml @@ -0,0 +1,30 @@ +post: + summary: Revoke enrollment token + x-auth-scopes: [admin] + parameters: + - name: token_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Revoked + content: + application/json: + schema: + $ref: '../schemas/enrollment-token.yaml' + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + '404': + description: Not Found + x-error-codes: [not_found] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' diff --git a/docs/architecture/openapi/paths/admin-enrollment-tokens.yaml b/docs/architecture/openapi/paths/admin-enrollment-tokens.yaml new file mode 100644 index 0000000..a84006a --- /dev/null +++ b/docs/architecture/openapi/paths/admin-enrollment-tokens.yaml @@ -0,0 +1,69 @@ +get: + summary: List enrollment tokens + x-auth-scopes: [admin] + parameters: + - name: project_id + in: query + required: false + schema: + type: string + - name: client_id + in: query + required: false + schema: + type: string + - name: status + in: query + required: false + schema: + type: string + enum: [active, used, expired, revoked] + responses: + '200': + description: Token list + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '../schemas/enrollment-token.yaml' + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' +post: + summary: Create enrollment token + x-auth-scopes: [admin] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../schemas/enrollment-token-create-request.yaml' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '../schemas/enrollment-token-create-response.yaml' + '400': + description: Bad Request + x-error-codes: [invalid_payload] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' diff --git a/docs/architecture/openapi/paths/artifact.yaml b/docs/architecture/openapi/paths/artifact.yaml new file mode 100644 index 0000000..55aada6 --- /dev/null +++ b/docs/architecture/openapi/paths/artifact.yaml @@ -0,0 +1,58 @@ +get: + summary: Download artifact + x-auth-scopes: [read_manifest] + parameters: + - name: project_id + in: path + required: true + schema: + type: string + - name: version + in: path + required: true + schema: + type: string + responses: + '200': + description: Artifact tar.gz + content: + application/gzip: + schema: + type: string + format: binary + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + unauthorized: + value: + code: unauthorized + message: Missing or invalid token + '404': + description: Not Found + x-error-codes: [not_found] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + not_found: + value: + code: not_found + message: Artifact not found + '429': + description: Too Many Requests + x-error-codes: [rate_limited] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + rate_limited: + value: + code: rate_limited + message: Too many requests diff --git a/docs/architecture/openapi/paths/enroll.yaml b/docs/architecture/openapi/paths/enroll.yaml new file mode 100644 index 0000000..42447df --- /dev/null +++ b/docs/architecture/openapi/paths/enroll.yaml @@ -0,0 +1,65 @@ +post: + summary: Enroll client and issue long-term token + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../schemas/enroll-request.yaml' + examples: + enroll: + value: + project_id: demo + client_id: device-42 + software_id: kiosk + enroll_token: enroll_6f3d2c... + responses: + '200': + description: Enrollment successful + content: + application/json: + schema: + $ref: '../schemas/enroll-response.yaml' + examples: + issued: + value: + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + scope: read_manifest report_status + expires_at: 2026-12-30T10:00:00Z + '400': + description: Bad Request + x-error-codes: [invalid_payload] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + invalid_payload: + value: + code: invalid_payload + message: Missing required fields + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + unauthorized: + value: + code: unauthorized + message: Invalid or expired enrollment token + '409': + description: Conflict + x-error-codes: [already_enrolled] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + already_enrolled: + value: + code: already_enrolled + message: Client already enrolled diff --git a/docs/architecture/openapi/paths/limits.yaml b/docs/architecture/openapi/paths/limits.yaml new file mode 100644 index 0000000..5647257 --- /dev/null +++ b/docs/architecture/openapi/paths/limits.yaml @@ -0,0 +1,48 @@ +get: + summary: Get service limits + x-auth-scopes: [read_manifest] + responses: + '200': + description: Limits + content: + application/json: + schema: + $ref: '../schemas/limits-policy.yaml' + examples: + medium: + value: + tier: medium + limits: + upload_max_artifact_size_bytes_soft: 1073741824 + upload_max_artifact_size_bytes_hard: 2147483648 + read_max_requests_per_minute_soft: 300 + read_max_requests_per_minute_hard: 600 + upload_max_requests_per_minute_soft: 6 + upload_max_requests_per_minute_hard: 12 + report_max_requests_per_minute_soft: 120 + report_max_requests_per_minute_hard: 240 + burst_requests_per_minute: 1200 + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + unauthorized: + value: + code: unauthorized + message: Missing or invalid token + '429': + description: Too Many Requests + x-error-codes: [rate_limited] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + rate_limited: + value: + code: rate_limited + message: Too many requests diff --git a/docs/architecture/openapi/paths/manifest.yaml b/docs/architecture/openapi/paths/manifest.yaml new file mode 100644 index 0000000..0ef9415 --- /dev/null +++ b/docs/architecture/openapi/paths/manifest.yaml @@ -0,0 +1,47 @@ +get: + summary: Get active manifest + x-auth-scopes: [read_manifest] + parameters: + - name: project_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Manifest + content: + application/json: + schema: + $ref: '../schemas/manifest.yaml' + examples: + default: + value: + version: 1.2.3 + artifact_url: https://update.wlkns.org/v1/projects/demo/releases/1.2.3/artifact + sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 + sig_url: https://update.wlkns.org/v1/projects/demo/releases/1.2.3/signature + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + unauthorized: + value: + code: unauthorized + message: Missing or invalid token + '429': + description: Too Many Requests + x-error-codes: [rate_limited] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + rate_limited: + value: + code: rate_limited + message: Too many requests diff --git a/docs/architecture/openapi/paths/releases.yaml b/docs/architecture/openapi/paths/releases.yaml new file mode 100644 index 0000000..0da343b --- /dev/null +++ b/docs/architecture/openapi/paths/releases.yaml @@ -0,0 +1,126 @@ +post: + summary: Upload release + x-auth-scopes: [upload_release] + parameters: + - name: project_id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - version + - sha256 + - artifact + properties: + version: + type: string + pattern: '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' + example: 1.2.3 + sha256: + type: string + example: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 + sig_url: + type: string + format: uri + description: Optional reference to a detached signature + signature: + type: string + format: binary + description: Detached signature file (optional alternative to sig_url) + key_id: + type: string + description: Public key identifier for signature verification + artifact: + type: string + format: binary + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '../schemas/upload-response.yaml' + examples: + created: + value: + version: 1.2.3 + manifest_url: https://update.wlkns.org/v1/projects/demo/manifest + active: true + '400': + description: Bad Request + x-error-codes: [invalid_payload] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + invalid_payload: + value: + code: invalid_payload + message: Missing required fields + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + unauthorized: + value: + code: unauthorized + message: Missing or invalid token + '409': + description: Conflict + x-error-codes: [version_exists] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + version_exists: + value: + code: version_exists + message: Version already exists + '413': + description: Payload Too Large + x-error-codes: [payload_too_large] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + payload_too_large: + value: + code: payload_too_large + message: Artifact exceeds size limit + '422': + description: Unprocessable Entity (invalid checksum/signature/version) + x-error-codes: [checksum_mismatch, signature_invalid, signature_missing, version_invalid] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + checksum_mismatch: + value: + code: checksum_mismatch + message: SHA256 does not match artifact + '429': + description: Too Many Requests + x-error-codes: [rate_limited] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + rate_limited: + value: + code: rate_limited + message: Too many requests diff --git a/docs/architecture/openapi/paths/status.yaml b/docs/architecture/openapi/paths/status.yaml new file mode 100644 index 0000000..2321caf --- /dev/null +++ b/docs/architecture/openapi/paths/status.yaml @@ -0,0 +1,84 @@ +post: + summary: Report update status + x-auth-scopes: [report_status] + parameters: + - name: project_id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '../schemas/status-report.yaml' + examples: + success: + value: + project_id: demo + version: 1.2.3 + status: success + timestamp: 2025-12-28T10:15:30Z + client_id: device-42 + duration_ms: 2450 + failure: + value: + project_id: demo + version: 1.2.3 + status: failed + timestamp: 2025-12-28T10:15:30Z + client_id: device-42 + reason: checksum_mismatch + error_code: checksum_mismatch + responses: + '202': + description: Accepted + '400': + description: Bad Request + x-error-codes: [invalid_payload] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + invalid_payload: + value: + code: invalid_payload + message: Missing required fields + '401': + description: Unauthorized + x-error-codes: [unauthorized] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + unauthorized: + value: + code: unauthorized + message: Missing or invalid token + '422': + description: Unprocessable Entity (invalid version or status) + x-error-codes: [version_invalid, status_invalid] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + version_invalid: + value: + code: version_invalid + message: Version does not match SemVer + '429': + description: Too Many Requests + x-error-codes: [rate_limited] + content: + application/json: + schema: + $ref: '../schemas/error.yaml' + examples: + rate_limited: + value: + code: rate_limited + message: Too many requests diff --git a/docs/architecture/openapi/schemas/enroll-request.yaml b/docs/architecture/openapi/schemas/enroll-request.yaml new file mode 100644 index 0000000..74834fa --- /dev/null +++ b/docs/architecture/openapi/schemas/enroll-request.yaml @@ -0,0 +1,20 @@ +type: object +required: + - project_id + - client_id + - software_id + - enroll_token +properties: + project_id: + type: string + example: demo + client_id: + type: string + example: device-42 + software_id: + type: string + example: kiosk + enroll_token: + type: string + description: Pre-shared, single-use enrollment token + example: enroll_6f3d2c... diff --git a/docs/architecture/openapi/schemas/enroll-response.yaml b/docs/architecture/openapi/schemas/enroll-response.yaml new file mode 100644 index 0000000..f8f72f4 --- /dev/null +++ b/docs/architecture/openapi/schemas/enroll-response.yaml @@ -0,0 +1,19 @@ +type: object +required: + - token + - scope +properties: + token: + type: string + description: Long-term bearer token for client requests + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + scope: + type: string + description: Space-delimited scopes + example: read_manifest report_status + expires_at: + type: string + format: date-time + nullable: true + description: Null for non-expiring tokens + example: 2026-12-30T10:00:00Z diff --git a/docs/architecture/openapi/schemas/enrollment-token-create-request.yaml b/docs/architecture/openapi/schemas/enrollment-token-create-request.yaml new file mode 100644 index 0000000..d78fb35 --- /dev/null +++ b/docs/architecture/openapi/schemas/enrollment-token-create-request.yaml @@ -0,0 +1,20 @@ +type: object +required: + - project_id +properties: + project_id: + type: string + example: demo + client_id: + type: string + nullable: true + example: device-42 + software_id: + type: string + nullable: true + example: kiosk + expires_at: + type: string + format: date-time + nullable: true + example: 2026-12-30T10:00:00Z diff --git a/docs/architecture/openapi/schemas/enrollment-token-create-response.yaml b/docs/architecture/openapi/schemas/enrollment-token-create-response.yaml new file mode 100644 index 0000000..0845f78 --- /dev/null +++ b/docs/architecture/openapi/schemas/enrollment-token-create-response.yaml @@ -0,0 +1,11 @@ +type: object +required: + - token + - token_meta +properties: + token: + type: string + description: One-time plaintext enrollment token + example: enroll_6f3d2c... + token_meta: + $ref: './enrollment-token.yaml' diff --git a/docs/architecture/openapi/schemas/enrollment-token.yaml b/docs/architecture/openapi/schemas/enrollment-token.yaml new file mode 100644 index 0000000..2ba063d --- /dev/null +++ b/docs/architecture/openapi/schemas/enrollment-token.yaml @@ -0,0 +1,39 @@ +type: object +required: + - id + - project_id + - status + - created_at +properties: + id: + type: string + example: tok_123 + project_id: + type: string + example: demo + client_id: + type: string + nullable: true + example: device-42 + software_id: + type: string + nullable: true + example: kiosk + status: + type: string + enum: [active, used, expired, revoked] + example: active + expires_at: + type: string + format: date-time + nullable: true + example: 2026-12-30T10:00:00Z + created_at: + type: string + format: date-time + example: 2025-12-30T10:00:00Z + used_at: + type: string + format: date-time + nullable: true + example: 2025-12-30T10:15:00Z diff --git a/docs/architecture/openapi/schemas/error.yaml b/docs/architecture/openapi/schemas/error.yaml new file mode 100644 index 0000000..a7fa4a3 --- /dev/null +++ b/docs/architecture/openapi/schemas/error.yaml @@ -0,0 +1,15 @@ +type: object +required: + - code + - message +properties: + code: + type: string + description: Error code (e.g., unauthorized, invalid_payload, already_enrolled) + example: unauthorized + message: + type: string + example: Missing or invalid token + details: + type: object + additionalProperties: true diff --git a/docs/architecture/openapi/schemas/limits-policy.yaml b/docs/architecture/openapi/schemas/limits-policy.yaml new file mode 100644 index 0000000..82a1606 --- /dev/null +++ b/docs/architecture/openapi/schemas/limits-policy.yaml @@ -0,0 +1,10 @@ +type: object +required: + - tier + - limits +properties: + tier: + type: string + enum: [small, medium, large] + limits: + $ref: './limits.yaml' diff --git a/docs/architecture/openapi/schemas/limits.yaml b/docs/architecture/openapi/schemas/limits.yaml new file mode 100644 index 0000000..99455e4 --- /dev/null +++ b/docs/architecture/openapi/schemas/limits.yaml @@ -0,0 +1,39 @@ +type: object +required: + - upload_max_artifact_size_bytes_soft + - upload_max_artifact_size_bytes_hard + - read_max_requests_per_minute_soft + - read_max_requests_per_minute_hard + - upload_max_requests_per_minute_soft + - upload_max_requests_per_minute_hard + - report_max_requests_per_minute_soft + - report_max_requests_per_minute_hard + - burst_requests_per_minute +properties: + upload_max_artifact_size_bytes_soft: + type: integer + default: 1073741824 + upload_max_artifact_size_bytes_hard: + type: integer + default: 2147483648 + read_max_requests_per_minute_soft: + type: integer + default: 300 + read_max_requests_per_minute_hard: + type: integer + default: 600 + upload_max_requests_per_minute_soft: + type: integer + default: 6 + upload_max_requests_per_minute_hard: + type: integer + default: 12 + report_max_requests_per_minute_soft: + type: integer + default: 120 + report_max_requests_per_minute_hard: + type: integer + default: 240 + burst_requests_per_minute: + type: integer + default: 1200 diff --git a/docs/architecture/openapi/schemas/manifest.yaml b/docs/architecture/openapi/schemas/manifest.yaml new file mode 100644 index 0000000..078e565 --- /dev/null +++ b/docs/architecture/openapi/schemas/manifest.yaml @@ -0,0 +1,24 @@ +type: object +required: + - version + - artifact_url + - sha256 +properties: + version: + type: string + description: SemVer string (e.g., 1.2.3) + pattern: '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' + example: 1.2.3 + artifact_url: + type: string + format: uri + example: https://update.wlkns.org/v1/projects/demo/releases/1.2.3/artifact + sha256: + type: string + description: Hex-encoded SHA256 + example: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 + sig_url: + type: string + format: uri + nullable: true + example: https://update.wlkns.org/v1/projects/demo/releases/1.2.3/signature diff --git a/docs/architecture/openapi/schemas/status-report.yaml b/docs/architecture/openapi/schemas/status-report.yaml new file mode 100644 index 0000000..09eca74 --- /dev/null +++ b/docs/architecture/openapi/schemas/status-report.yaml @@ -0,0 +1,42 @@ +type: object +required: + - project_id + - version + - status + - timestamp +properties: + project_id: + type: string + example: demo + version: + type: string + example: 1.2.3 + status: + type: string + enum: [success, failed, in_progress] + example: success + timestamp: + type: string + format: date-time + example: 2025-12-28T10:15:30Z + reason: + type: string + example: checksum_mismatch + client_id: + type: string + example: device-42 + client_version: + type: string + example: 1.2.2 + device_type: + type: string + example: kiosk + update_channel: + type: string + example: stable + duration_ms: + type: integer + example: 2450 + error_code: + type: string + example: checksum_mismatch diff --git a/docs/architecture/openapi/schemas/upload-response.yaml b/docs/architecture/openapi/schemas/upload-response.yaml new file mode 100644 index 0000000..ff933f2 --- /dev/null +++ b/docs/architecture/openapi/schemas/upload-response.yaml @@ -0,0 +1,16 @@ +type: object +required: + - version + - manifest_url +properties: + version: + type: string + example: 1.2.3 + manifest_url: + type: string + format: uri + example: https://update.wlkns.org/v1/projects/demo/manifest + active: + type: boolean + description: True if release is active + example: true diff --git a/docs/client-quickstart.md b/docs/client-quickstart.md new file mode 100644 index 0000000..838314b --- /dev/null +++ b/docs/client-quickstart.md @@ -0,0 +1,44 @@ +ID: DOC_000008 | Version: 0.1.0 | Status: Draft + +# Client Quickstart + +## Goal +Enroll a client, store the long-term token, fetch the manifest, and report status. + +## 1) Get a Pre-Shared Token +Request a one-time enrollment token from an admin/operator. + +## 2) Enroll and Receive Long-Term Token +``` +curl -X POST https://update.wlkns.org/v1/enroll \ + -H "Content-Type: application/json" \ + -d '{ + "project_id": "safe-kiddo-control", + "client_id": "kiddo-001", + "software_id": "kiddo-agent", + "enroll_token": "" + }' +``` + +Store the returned token locally (file or secret store). Example: +``` +echo "" > ./update-token.txt +``` + +## 3) Fetch Manifest +``` +curl -H "Authorization: Bearer $(cat ./update-token.txt)" \ + https://update.wlkns.org/v1/projects/safe-kiddo-control/manifest +``` + +## 4) Report Status +``` +curl -H "Authorization: Bearer $(cat ./update-token.txt)" \ + -H "Content-Type: application/json" \ + -d '{"project_id":"safe-kiddo-control","version":"0.1.2","status":"success","timestamp":"2025-12-30T10:00:00Z"}' \ + https://update.wlkns.org/v1/projects/safe-kiddo-control/status +``` + +## Notes +- All endpoints require `Authorization: Bearer ` except `/v1/enroll`. +- Status values: `success`, `failed`, `in_progress`. diff --git a/docs/third-party-api.md b/docs/third-party-api.md new file mode 100644 index 0000000..d53aa40 --- /dev/null +++ b/docs/third-party-api.md @@ -0,0 +1,135 @@ +ID: DOC_000005 | Version: 0.1.0 | Status: Draft + +# Third-Party API Guide + +## Purpose +This document explains how third-party services integrate with the Update Webservice: obtaining tokens, fetching manifests, downloading artifacts, and reporting status. + +## Quick Start (First Client) +1) Request a pre-shared enrollment token from an admin/operator. +2) Enroll once to obtain a long-term token. +3) Store the long-term token locally and use it for all API calls. + +## Base URLs +- Production: `https://update.wlkns.org` +- Staging: `https://staging.update.wlkns.org` + +All endpoints are versioned under `/v1`. + +## Authentication +All endpoints require `Authorization: Bearer `. + +### Enrollment (Pre-Shared Token -> Long-Term Token) +Clients obtain a long-term token by exchanging a pre-shared token provided by an admin/operator. + +Request (example): +``` +POST /v1/enroll +{ + "project_id": "", + "client_id": "", + "software_id": "", + "enroll_token": "" +} +``` + +Response (example): +``` +200 OK +{ + "token": "", + "scope": "read_manifest report_status", + "expires_at": "" +} +``` + +Notes: +- Enrollment tokens are single-use and must be invalidated after a successful exchange. +- If the token is invalid or reused, the server responds with `unauthorized` or `invalid_payload`. +- Enrollment does not require an existing bearer token. + - If the client is already enrolled, the server responds with `already_enrolled` (HTTP 409). + +## Client API (Read + Report) + +### Get Manifest +``` +GET /v1/projects/{project_id}/manifest +``` + +Response: +``` +{ + "version": "0.1.2", + "artifact_url": "https://update.wlkns.org/v1/projects//releases/0.1.2/artifact", + "sha256": "", + "sig_url": "" +} +``` + +Required scope: `read_manifest` + +### Download Artifact +``` +GET /v1/projects/{project_id}/releases/{version}/artifact +``` + +Required scope: `read_manifest` + +### Report Status +``` +POST /v1/projects/{project_id}/status +{ + "project_id": "", + "version": "", + "status": "success|failed|in_progress", + "timestamp": "", + "client_id": "", + "duration_ms": "", + "error_code": "" +} +``` + +Required scope: `report_status` + +## Release API (Upload) + +### Upload Release +``` +POST /v1/projects/{project_id}/releases +Content-Type: multipart/form-data +``` + +Required scope: `upload_release` + +Required fields: +- `version` (SemVer) +- `artifact` (file) +- `sha256` (hex) + +Optional fields: +- `sig_url` or inline signature +- `key_id` + +## Error Codes +Common error codes: +`unauthorized`, `rate_limited`, `not_found`, `invalid_payload`, `version_invalid`, +`version_exists`, `checksum_mismatch`, `signature_missing`, `signature_invalid`, +`payload_too_large`, `status_invalid` + +## Rate Limits +Limits are tiered by scope. See `docs/architecture/ARCHITECTURE.md` for current values. + +## Examples +Fetch manifest: +``` +curl -H "Authorization: Bearer $TOKEN" \ + https://update.wlkns.org/v1/projects/$PROJECT_ID/manifest +``` + +Report status: +``` +curl -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"project_id":"'"$PROJECT_ID"'","version":"0.1.2","status":"success","timestamp":"2025-12-30T10:00:00Z"}' \ + https://update.wlkns.org/v1/projects/$PROJECT_ID/status +``` diff --git a/project-management/PROJECT_STATUS.md b/project-management/PROJECT_STATUS.md index 076fca0..006ddfa 100644 --- a/project-management/PROJECT_STATUS.md +++ b/project-management/PROJECT_STATUS.md @@ -108,6 +108,12 @@ Sicheres, remote steuerbares System zum Sperren/Entsperren lokaler Nutzerkonten. - [ ] US_000027: Client verifiziert und wendet Updates an - [ ] US_000028: Client meldet Update-Status +### EPIC_000010: Update-Service v1 Migration (Major Release) +- [x] US_000034: Enrollment fuer Langzeit-Token +- [x] TASK_000040: Enrollment-Flow implementieren +- [x] US_000035: v1 Update-Endpoints und Status-Schema +- [x] TASK_000041: v1 Endpunkte im Update-Client umstellen + ## Offene Risiken / Abhaengigkeiten - Betrieb erfordert Root/sudo und lokale System-Tools (notify-send, sound player, uvicorn). - OIDC-Validierung blockiert bis IdP bereit und Service laeuft. diff --git a/project-management/feedback/teal/app_icon.svg b/project-management/feedback/teal/app_icon.svg new file mode 100644 index 0000000..07c44a7 --- /dev/null +++ b/project-management/feedback/teal/app_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/project-management/feedback/teal/app_icon_ai.svg b/project-management/feedback/teal/app_icon_ai.svg new file mode 100644 index 0000000..699f9fc --- /dev/null +++ b/project-management/feedback/teal/app_icon_ai.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/project-management/feedback/teal/favicon.svg b/project-management/feedback/teal/favicon.svg new file mode 100644 index 0000000..1bf5b24 --- /dev/null +++ b/project-management/feedback/teal/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/project-management/feedback/teal/logo.svg b/project-management/feedback/teal/logo.svg new file mode 100644 index 0000000..37aa412 --- /dev/null +++ b/project-management/feedback/teal/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + WLKNS + + + + \ No newline at end of file diff --git a/project-management/feedback/teal/logo_ai.svg b/project-management/feedback/teal/logo_ai.svg new file mode 100644 index 0000000..ab50f00 --- /dev/null +++ b/project-management/feedback/teal/logo_ai.svg @@ -0,0 +1,16 @@ + + + + + + + + WLKNS + + + + AI + + + + \ No newline at end of file diff --git a/project-management/requirements/epics/EPIC_000010.md b/project-management/requirements/epics/EPIC_000010.md index 5db910f..563cfba 100644 --- a/project-management/requirements/epics/EPIC_000010.md +++ b/project-management/requirements/epics/EPIC_000010.md @@ -1,4 +1,4 @@ -ID: EPIC_000010 | Version: 0.1.5 | Status: Draft +ID: EPIC_000010 | Version: 0.1.5 | Status: Done By: Codex (GPT-5) # EPIC_000010: Update-Service v1 Migration (Major Release) diff --git a/project-management/requirements/stories/US_000034.md b/project-management/requirements/stories/US_000034.md index ec4373f..8ed644f 100644 --- a/project-management/requirements/stories/US_000034.md +++ b/project-management/requirements/stories/US_000034.md @@ -1,4 +1,4 @@ -ID: US_000034 | Version: 0.1.5 | Status: Draft +ID: US_000034 | Version: 0.1.5 | Status: Done By: Codex (GPT-5) # US_000034: Enrollment fuer Langzeit-Token diff --git a/project-management/requirements/stories/US_000035.md b/project-management/requirements/stories/US_000035.md index 9ad8fdb..f9adcd1 100644 --- a/project-management/requirements/stories/US_000035.md +++ b/project-management/requirements/stories/US_000035.md @@ -1,4 +1,4 @@ -ID: US_000035 | Version: 0.1.5 | Status: Draft +ID: US_000035 | Version: 0.1.5 | Status: Done By: Codex (GPT-5) # US_000035: v1 Update-Endpoints und Status-Schema diff --git a/project-management/requirements/tasks/TASK_000040.md b/project-management/requirements/tasks/TASK_000040.md index 35ffbff..c41c16a 100644 --- a/project-management/requirements/tasks/TASK_000040.md +++ b/project-management/requirements/tasks/TASK_000040.md @@ -1,4 +1,4 @@ -ID: TASK_000040 | Version: 0.1.5 | Status: Draft +ID: TASK_000040 | Version: 0.1.5 | Status: Done By: Codex (GPT-5) # TASK_000040: Enrollment-Flow implementieren diff --git a/project-management/requirements/tasks/TASK_000041.md b/project-management/requirements/tasks/TASK_000041.md index 5f43281..f087ca4 100644 --- a/project-management/requirements/tasks/TASK_000041.md +++ b/project-management/requirements/tasks/TASK_000041.md @@ -1,4 +1,4 @@ -ID: TASK_000041 | Version: 0.1.5 | Status: Draft +ID: TASK_000041 | Version: 0.1.5 | Status: Done By: Codex (GPT-5) # TASK_000041: v1 Endpunkte im Update-Client umstellen diff --git a/scripts/rollback_client.sh b/scripts/rollback_client.sh index 85026b2..262031d 100755 --- a/scripts/rollback_client.sh +++ b/scripts/rollback_client.sh @@ -2,6 +2,10 @@ set -euo pipefail SERVICE_NAME="${SERVICE_NAME:-skd}" +UPDATE_SERVICE_URL="${SKD_UPDATE_SERVICE_URL:-https://update.wlkns.org}" +PROJECT_ID="${SKD_UPDATE_PROJECT_ID:-safe-kiddo-control}" +UPDATE_TOKEN="${SKD_UPDATE_TOKEN:-}" +STATUS_URL="${UPDATE_SERVICE_URL}/v1/projects/${PROJECT_ID}/status" INSTALL_DIR="${INSTALL_DIR:-/opt/sk}" STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}" LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}" @@ -14,6 +18,8 @@ write_status() { local status="$1" local error="${2:-}" local version="$3" + local timestamp + timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS="${status}" SKD_ERROR="${error}" SKD_VERSION="${version}" \ SKD_STATUS_FILE="${STATUS_FILE}" SKD_LOG_FILE="${LOG_FILE}" python3 - <<'PY' import json @@ -41,7 +47,25 @@ status_file.write_text(json.dumps(payload), encoding="utf-8") with log_file.open("a", encoding="utf-8") as handle: handle.write(json.dumps(payload) + "\n") PY + local payload + payload=$(cat </dev/null || true + fi +} + LATEST_BACKUP="$(ls -dt /opt/sk_backup_* 2>/dev/null | head -1 || true)" if [[ -z "${LATEST_BACKUP}" ]]; then diff --git a/scripts/update_client.sh b/scripts/update_client.sh index d3b4782..5831736 100755 --- a/scripts/update_client.sh +++ b/scripts/update_client.sh @@ -2,9 +2,11 @@ set -euo pipefail SERVICE_NAME="${SERVICE_NAME:-skd}" -UPDATE_URL="${SKD_UPDATE_URL:-https://update.wlkns.org}" +UPDATE_SERVICE_URL="${SKD_UPDATE_SERVICE_URL:-https://update.wlkns.org}" +PROJECT_ID="${SKD_UPDATE_PROJECT_ID:-safe-kiddo-control}" UPDATE_TOKEN="${SKD_UPDATE_TOKEN:-}" -STATUS_URL="${SKD_UPDATE_STATUS_URL:-https://update.wlkns.org/status}" +MANIFEST_URL="${UPDATE_SERVICE_URL}/v1/projects/${PROJECT_ID}/manifest" +STATUS_URL="${UPDATE_SERVICE_URL}/v1/projects/${PROJECT_ID}/status" STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}" LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}" INSTALL_DIR="${INSTALL_DIR:-/opt/sk}" @@ -53,7 +55,15 @@ with log_file.open("a", encoding="utf-8") as handle: PY local payload payload=$(cat <