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
This commit is contained in:
@ -19,6 +19,8 @@ from backend.auth import (
|
|||||||
from backend.models import (
|
from backend.models import (
|
||||||
ActionRequest,
|
ActionRequest,
|
||||||
ActionResponse,
|
ActionResponse,
|
||||||
|
EnrollRequest,
|
||||||
|
EnrollResponse,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
UpdateActionResponse,
|
UpdateActionResponse,
|
||||||
@ -239,6 +241,24 @@ def update_status(settings: Settings = Depends(get_settings)) -> UpdateStatus:
|
|||||||
return UpdateStatus(**status_data)
|
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)])
|
@app.post("/update/check", response_model=UpdateCheckResponse, dependencies=[Depends(get_current_admin)])
|
||||||
def update_check(settings: Settings = Depends(get_settings)) -> UpdateCheckResponse:
|
def update_check(settings: Settings = Depends(get_settings)) -> UpdateCheckResponse:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -41,6 +41,16 @@ class UpdateStatus(BaseModel):
|
|||||||
last_status: str
|
last_status: str
|
||||||
last_error: Optional[str] = None
|
last_error: Optional[str] = None
|
||||||
last_timestamp: 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):
|
class UpdateCheckResponse(BaseModel):
|
||||||
|
|||||||
@ -40,7 +40,13 @@ class Settings:
|
|||||||
self.notify_timeout: int = int(os.getenv("SKD_NOTIFY_TIMEOUT", "5"))
|
self.notify_timeout: int = int(os.getenv("SKD_NOTIFY_TIMEOUT", "5"))
|
||||||
self.dry_run: bool = os.getenv("SKD_DRY_RUN", "false").lower() == "true"
|
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_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_interval: int = int(os.getenv("SKD_UPDATE_INTERVAL", "3600"))
|
||||||
self.update_status_url: str = os.getenv(
|
self.update_status_url: str = os.getenv(
|
||||||
"SKD_UPDATE_STATUS_URL", "https://update.wlkns.org/status"
|
"SKD_UPDATE_STATUS_URL", "https://update.wlkns.org/status"
|
||||||
@ -63,6 +69,18 @@ class Settings:
|
|||||||
def _parse_list(value: str) -> List[str]:
|
def _parse_list(value: str) -> List[str]:
|
||||||
return [item for item in (part.strip() for part in value.split(",")) if item]
|
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)
|
@lru_cache(maxsize=1)
|
||||||
def get_settings() -> Settings:
|
def get_settings() -> Settings:
|
||||||
|
|||||||
@ -70,20 +70,56 @@ def get_status(settings: Settings) -> Dict[str, Any]:
|
|||||||
"last_status": data.get("status", "unknown"),
|
"last_status": data.get("status", "unknown"),
|
||||||
"last_error": data.get("error") or None,
|
"last_error": data.get("error") or None,
|
||||||
"last_timestamp": data.get("timestamp"),
|
"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]:
|
def _parse_version(value: str) -> List[int]:
|
||||||
return [int(part) for part in value.split(".")]
|
return [int(part) for part in value.split(".")]
|
||||||
|
|
||||||
|
|
||||||
def check_update(settings: Settings) -> Dict[str, Any]:
|
def check_update(settings: Settings) -> Dict[str, Any]:
|
||||||
headers = {}
|
if not settings.update_token:
|
||||||
if settings.update_token:
|
raise ValueError("Client is not enrolled (missing update token)")
|
||||||
headers["Authorization"] = f"Bearer {settings.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:
|
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()
|
response.raise_for_status()
|
||||||
manifest = response.json()
|
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:
|
def _run_async(script_path: Path, settings: Settings) -> None:
|
||||||
env = os.environ.copy()
|
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_STATUS_FILE"] = settings.update_status_file
|
||||||
env["SKD_UPDATE_LOG_FILE"] = settings.update_log_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:
|
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"
|
script = _project_root() / "scripts" / "update_client.sh"
|
||||||
_run_async(script, settings)
|
_run_async(script, settings)
|
||||||
|
|
||||||
|
|
||||||
def start_rollback(settings: Settings) -> None:
|
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"
|
script = _project_root() / "scripts" / "rollback_client.sh"
|
||||||
_run_async(script, settings)
|
_run_async(script, settings)
|
||||||
|
|
||||||
|
|||||||
91
docs/admin-token-operations.md
Normal file
91
docs/admin-token-operations.md
Normal file
@ -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 <ADMIN_TOKEN>`.
|
||||||
|
- 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 <id> [--client <id>] [--software <id>] [--expires <iso8601>]`
|
||||||
|
- `update-service admin token list --project <id> [--status active|used|expired|revoked]`
|
||||||
|
- `update-service admin token revoke --id <token_id>`
|
||||||
|
- `update-service admin token export --id <token_id> --out ./enroll-token.txt`
|
||||||
|
|
||||||
|
Example format:
|
||||||
|
```
|
||||||
|
enroll_<random_32_bytes>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
57
docs/architecture/openapi.yaml
Normal file
57
docs/architecture/openapi.yaml
Normal file
@ -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'
|
||||||
@ -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'
|
||||||
@ -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'
|
||||||
69
docs/architecture/openapi/paths/admin-enrollment-tokens.yaml
Normal file
69
docs/architecture/openapi/paths/admin-enrollment-tokens.yaml
Normal file
@ -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'
|
||||||
58
docs/architecture/openapi/paths/artifact.yaml
Normal file
58
docs/architecture/openapi/paths/artifact.yaml
Normal file
@ -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
|
||||||
65
docs/architecture/openapi/paths/enroll.yaml
Normal file
65
docs/architecture/openapi/paths/enroll.yaml
Normal file
@ -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
|
||||||
48
docs/architecture/openapi/paths/limits.yaml
Normal file
48
docs/architecture/openapi/paths/limits.yaml
Normal file
@ -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
|
||||||
47
docs/architecture/openapi/paths/manifest.yaml
Normal file
47
docs/architecture/openapi/paths/manifest.yaml
Normal file
@ -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
|
||||||
126
docs/architecture/openapi/paths/releases.yaml
Normal file
126
docs/architecture/openapi/paths/releases.yaml
Normal file
@ -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
|
||||||
84
docs/architecture/openapi/paths/status.yaml
Normal file
84
docs/architecture/openapi/paths/status.yaml
Normal file
@ -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
|
||||||
20
docs/architecture/openapi/schemas/enroll-request.yaml
Normal file
20
docs/architecture/openapi/schemas/enroll-request.yaml
Normal file
@ -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...
|
||||||
19
docs/architecture/openapi/schemas/enroll-response.yaml
Normal file
19
docs/architecture/openapi/schemas/enroll-response.yaml
Normal file
@ -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
|
||||||
@ -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
|
||||||
@ -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'
|
||||||
39
docs/architecture/openapi/schemas/enrollment-token.yaml
Normal file
39
docs/architecture/openapi/schemas/enrollment-token.yaml
Normal file
@ -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
|
||||||
15
docs/architecture/openapi/schemas/error.yaml
Normal file
15
docs/architecture/openapi/schemas/error.yaml
Normal file
@ -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
|
||||||
10
docs/architecture/openapi/schemas/limits-policy.yaml
Normal file
10
docs/architecture/openapi/schemas/limits-policy.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- tier
|
||||||
|
- limits
|
||||||
|
properties:
|
||||||
|
tier:
|
||||||
|
type: string
|
||||||
|
enum: [small, medium, large]
|
||||||
|
limits:
|
||||||
|
$ref: './limits.yaml'
|
||||||
39
docs/architecture/openapi/schemas/limits.yaml
Normal file
39
docs/architecture/openapi/schemas/limits.yaml
Normal file
@ -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
|
||||||
24
docs/architecture/openapi/schemas/manifest.yaml
Normal file
24
docs/architecture/openapi/schemas/manifest.yaml
Normal file
@ -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
|
||||||
42
docs/architecture/openapi/schemas/status-report.yaml
Normal file
42
docs/architecture/openapi/schemas/status-report.yaml
Normal file
@ -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
|
||||||
16
docs/architecture/openapi/schemas/upload-response.yaml
Normal file
16
docs/architecture/openapi/schemas/upload-response.yaml
Normal file
@ -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
|
||||||
44
docs/client-quickstart.md
Normal file
44
docs/client-quickstart.md
Normal file
@ -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": "<pre_shared_token>"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Store the returned token locally (file or secret store). Example:
|
||||||
|
```
|
||||||
|
echo "<long_term_token>" > ./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 <token>` except `/v1/enroll`.
|
||||||
|
- Status values: `success`, `failed`, `in_progress`.
|
||||||
135
docs/third-party-api.md
Normal file
135
docs/third-party-api.md
Normal file
@ -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 <token>`.
|
||||||
|
|
||||||
|
### 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": "<project>",
|
||||||
|
"client_id": "<client>",
|
||||||
|
"software_id": "<software>",
|
||||||
|
"enroll_token": "<pre_shared_token>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response (example):
|
||||||
|
```
|
||||||
|
200 OK
|
||||||
|
{
|
||||||
|
"token": "<long_term_token>",
|
||||||
|
"scope": "read_manifest report_status",
|
||||||
|
"expires_at": "<iso8601 or null>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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/<project_id>/releases/0.1.2/artifact",
|
||||||
|
"sha256": "<hex>",
|
||||||
|
"sig_url": "<optional>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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": "<project_id>",
|
||||||
|
"version": "<semver>",
|
||||||
|
"status": "success|failed|in_progress",
|
||||||
|
"timestamp": "<iso8601>",
|
||||||
|
"client_id": "<optional>",
|
||||||
|
"duration_ms": "<optional>",
|
||||||
|
"error_code": "<optional>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
@ -108,6 +108,12 @@ Sicheres, remote steuerbares System zum Sperren/Entsperren lokaler Nutzerkonten.
|
|||||||
- [ ] US_000027: Client verifiziert und wendet Updates an
|
- [ ] US_000027: Client verifiziert und wendet Updates an
|
||||||
- [ ] US_000028: Client meldet Update-Status
|
- [ ] 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
|
## Offene Risiken / Abhaengigkeiten
|
||||||
- Betrieb erfordert Root/sudo und lokale System-Tools (notify-send, sound player, uvicorn).
|
- Betrieb erfordert Root/sudo und lokale System-Tools (notify-send, sound player, uvicorn).
|
||||||
- OIDC-Validierung blockiert bis IdP bereit und Service laeuft.
|
- OIDC-Validierung blockiert bis IdP bereit und Service laeuft.
|
||||||
|
|||||||
10
project-management/feedback/teal/app_icon.svg
Normal file
10
project-management/feedback/teal/app_icon.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- V2 App Icon (Teal Optimized) -->
|
||||||
|
<rect width="512" height="512" rx="115" fill="#1F2A37"/>
|
||||||
|
<g transform="translate(120, 96) scale(8)">
|
||||||
|
<rect x="0" y="4" width="10" height="32" rx="1.5" fill="#FFFFFF" fill-opacity="0.9"/>
|
||||||
|
<rect x="24" y="4" width="10" height="32" rx="1.5" fill="#FFFFFF" fill-opacity="0.9"/>
|
||||||
|
<!-- Teal Keystone -->
|
||||||
|
<rect x="12" y="17" width="10" height="19" rx="1.5" fill="#0EA5A4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 539 B |
9
project-management/feedback/teal/app_icon_ai.svg
Normal file
9
project-management/feedback/teal/app_icon_ai.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- V12 App Icon (Teal) -->
|
||||||
|
<rect width="512" height="512" rx="115" fill="#1F2A37"/>
|
||||||
|
<g transform="translate(120, 96) scale(8)">
|
||||||
|
<rect x="0" y="4" width="10" height="32" rx="1.5" fill="#FFFFFF" fill-opacity="0.9"/>
|
||||||
|
<rect x="24" y="4" width="10" height="32" rx="1.5" fill="#FFFFFF" fill-opacity="0.9"/>
|
||||||
|
<rect x="12" y="17" width="10" height="19" rx="1.5" fill="#0EA5A4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 503 B |
7
project-management/feedback/teal/favicon.svg
Normal file
7
project-management/feedback/teal/favicon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="64" height="64" rx="12" fill="#1F2A37"/>
|
||||||
|
<rect x="12" y="16" width="12" height="32" rx="2" fill="#FFFFFF" fill-opacity="0.9"/>
|
||||||
|
<rect x="40" y="16" width="12" height="32" rx="2" fill="#FFFFFF" fill-opacity="0.9"/>
|
||||||
|
<!-- Teal Keystone -->
|
||||||
|
<rect x="26" y="29" width="12" height="19" rx="2" fill="#0EA5A4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 428 B |
15
project-management/feedback/teal/logo.svg
Normal file
15
project-management/feedback/teal/logo.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg width="250" height="60" viewBox="0 0 250 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Monolithic Symbol (V2 Optimized - Teal) -->
|
||||||
|
<g transform="translate(10, 10)">
|
||||||
|
<rect x="0" y="4" width="10" height="32" rx="1.5" fill="#1F2A37"/>
|
||||||
|
<rect x="24" y="4" width="10" height="32" rx="1.5" fill="#1F2A37"/>
|
||||||
|
<!-- Middle Block: Teal, 19px Height -->
|
||||||
|
<rect x="12" y="17" width="10" height="19" rx="1.5" fill="#0EA5A4"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Wordmark -->
|
||||||
|
<text x="55" y="42" fill="#1F2A37" font-family="'Inter', sans-serif" font-weight="800" font-size="32" letter-spacing="-0.04em" text-anchor="start">WLKNS</text>
|
||||||
|
|
||||||
|
<!-- The Underline: Teal, aligned like V8 -->
|
||||||
|
<rect x="56" y="48" width="121" height="4" rx="1" fill="#0EA5A4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 764 B |
16
project-management/feedback/teal/logo_ai.svg
Normal file
16
project-management/feedback/teal/logo_ai.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<svg width="340" height="60" viewBox="0 0 340 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- V12: ALL TEAL -->
|
||||||
|
<g transform="translate(10, 10)">
|
||||||
|
<rect x="0" y="4" width="10" height="32" rx="1.5" fill="#1F2A37"/>
|
||||||
|
<rect x="24" y="4" width="10" height="32" rx="1.5" fill="#1F2A37"/>
|
||||||
|
<rect x="12" y="17" width="10" height="19" rx="1.5" fill="#0EA5A4"/>
|
||||||
|
</g>
|
||||||
|
<text x="55" y="42" fill="#1F2A37" font-family="'Inter', sans-serif" font-weight="800" font-size="32" letter-spacing="-0.04em" text-anchor="start">WLKNS</text>
|
||||||
|
<rect x="188" y="27" width="6" height="6" rx="1" fill="#4B5563"/>
|
||||||
|
|
||||||
|
<!-- AI: Teal -->
|
||||||
|
<text x="206" y="42" fill="#0EA5A4" font-family="'Inter', sans-serif" font-weight="800" font-size="32" letter-spacing="-0.04em" text-anchor="start">AI</text>
|
||||||
|
|
||||||
|
<!-- Underline: Teal -->
|
||||||
|
<rect x="56" y="48" width="186" height="4" rx="1" fill="#0EA5A4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 900 B |
@ -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)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000010: Update-Service v1 Migration (Major Release)
|
# EPIC_000010: Update-Service v1 Migration (Major Release)
|
||||||
|
|||||||
@ -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)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000034: Enrollment fuer Langzeit-Token
|
# US_000034: Enrollment fuer Langzeit-Token
|
||||||
|
|||||||
@ -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)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000035: v1 Update-Endpoints und Status-Schema
|
# US_000035: v1 Update-Endpoints und Status-Schema
|
||||||
|
|||||||
@ -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)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# TASK_000040: Enrollment-Flow implementieren
|
# TASK_000040: Enrollment-Flow implementieren
|
||||||
|
|||||||
@ -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)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# TASK_000041: v1 Endpunkte im Update-Client umstellen
|
# TASK_000041: v1 Endpunkte im Update-Client umstellen
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SERVICE_NAME="${SERVICE_NAME:-skd}"
|
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}"
|
INSTALL_DIR="${INSTALL_DIR:-/opt/sk}"
|
||||||
STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}"
|
STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}"
|
||||||
LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}"
|
LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}"
|
||||||
@ -14,6 +18,8 @@ write_status() {
|
|||||||
local status="$1"
|
local status="$1"
|
||||||
local error="${2:-}"
|
local error="${2:-}"
|
||||||
local version="$3"
|
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="${status}" SKD_ERROR="${error}" SKD_VERSION="${version}" \
|
||||||
SKD_STATUS_FILE="${STATUS_FILE}" SKD_LOG_FILE="${LOG_FILE}" python3 - <<'PY'
|
SKD_STATUS_FILE="${STATUS_FILE}" SKD_LOG_FILE="${LOG_FILE}" python3 - <<'PY'
|
||||||
import json
|
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:
|
with log_file.open("a", encoding="utf-8") as handle:
|
||||||
handle.write(json.dumps(payload) + "\n")
|
handle.write(json.dumps(payload) + "\n")
|
||||||
PY
|
PY
|
||||||
|
local payload
|
||||||
|
payload=$(cat <<JSON
|
||||||
|
{
|
||||||
|
"project_id": "${PROJECT_ID}",
|
||||||
|
"version": "${version}",
|
||||||
|
"status": "${status}",
|
||||||
|
"timestamp": "${timestamp}",
|
||||||
|
"client_id": "$(hostname)",
|
||||||
|
"error_code": "${error}",
|
||||||
|
"reason": "${error}"
|
||||||
}
|
}
|
||||||
|
JSON
|
||||||
|
)
|
||||||
|
if [[ -n "${UPDATE_TOKEN}" ]]; then
|
||||||
|
curl -sS -X POST -H "Authorization: Bearer ${UPDATE_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "${payload}" "${STATUS_URL}" >/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
LATEST_BACKUP="$(ls -dt /opt/sk_backup_* 2>/dev/null | head -1 || true)"
|
LATEST_BACKUP="$(ls -dt /opt/sk_backup_* 2>/dev/null | head -1 || true)"
|
||||||
if [[ -z "${LATEST_BACKUP}" ]]; then
|
if [[ -z "${LATEST_BACKUP}" ]]; then
|
||||||
|
|||||||
@ -2,9 +2,11 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SERVICE_NAME="${SERVICE_NAME:-skd}"
|
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:-}"
|
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}"
|
STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}"
|
||||||
LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}"
|
LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}"
|
||||||
INSTALL_DIR="${INSTALL_DIR:-/opt/sk}"
|
INSTALL_DIR="${INSTALL_DIR:-/opt/sk}"
|
||||||
@ -53,7 +55,15 @@ with log_file.open("a", encoding="utf-8") as handle:
|
|||||||
PY
|
PY
|
||||||
local payload
|
local payload
|
||||||
payload=$(cat <<JSON
|
payload=$(cat <<JSON
|
||||||
{"device_id":"$(hostname)","version":"${VERSION}","status":"${status}","error":"${error}","timestamp":"${timestamp}"}
|
{
|
||||||
|
"project_id": "${PROJECT_ID}",
|
||||||
|
"version": "${VERSION}",
|
||||||
|
"status": "${status}",
|
||||||
|
"timestamp": "${timestamp}",
|
||||||
|
"client_id": "$(hostname)",
|
||||||
|
"error_code": "${error}",
|
||||||
|
"reason": "${error}"
|
||||||
|
}
|
||||||
JSON
|
JSON
|
||||||
)
|
)
|
||||||
if [[ -n "${UPDATE_TOKEN}" ]]; then
|
if [[ -n "${UPDATE_TOKEN}" ]]; then
|
||||||
@ -89,12 +99,12 @@ require_cmd tar
|
|||||||
require_cmd sha256sum
|
require_cmd sha256sum
|
||||||
require_cmd python3
|
require_cmd python3
|
||||||
|
|
||||||
log "Fetching update manifest from ${UPDATE_URL}..."
|
log "Fetching update manifest from ${MANIFEST_URL}..."
|
||||||
MANIFEST_FILE="$(mktemp)"
|
MANIFEST_FILE="$(mktemp)"
|
||||||
if [[ -n "${UPDATE_TOKEN}" ]]; then
|
if [[ -n "${UPDATE_TOKEN}" ]]; then
|
||||||
curl -fsS -H "Authorization: Bearer ${UPDATE_TOKEN}" "${UPDATE_URL}" -o "${MANIFEST_FILE}"
|
curl -fsS -H "Authorization: Bearer ${UPDATE_TOKEN}" "${MANIFEST_URL}" -o "${MANIFEST_FILE}"
|
||||||
else
|
else
|
||||||
curl -fsS "${UPDATE_URL}" -o "${MANIFEST_FILE}"
|
curl -fsS "${MANIFEST_URL}" -o "${MANIFEST_FILE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read_manifest() {
|
read_manifest() {
|
||||||
|
|||||||
Reference in New Issue
Block a user