Compare commits
4 Commits
e75a989c54
...
b2edb400aa
| Author | SHA1 | Date | |
|---|---|---|---|
| b2edb400aa | |||
| 47290d2d8f | |||
| fde2825112 | |||
| f639e3c56a |
@ -47,6 +47,8 @@ By: Codex (GPT-5)
|
||||
| 30.12.2025 | 🎨 UI | ID: TASK_000037 OIDC-Button neben Anmelden und nur aktiv bei erreichbarem Server. By: Codex (GPT-5) |
|
||||
| 30.12.2025 | 🎨 UI | ID: TASK_000038 Logo im Header und Favicon eingebunden. By: Codex (GPT-5) |
|
||||
| 30.12.2025 | 🎨 UI | ID: TASK_000039 Login-Text reduziert, Buttons symmetrisch, Panels/Metrics harmonisiert. By: Codex (GPT-5) |
|
||||
| 30.12.2025 | 🏗️ Planning | ID: EPIC_000010/US_000034/US_000035 Update-Service v1 Migration dokumentiert. By: Codex (GPT-5) |
|
||||
| 30.12.2025 | 🏗️ Planning | ID: TASK_000040/TASK_000041 fuer Enrollment und v1 Endpunkte angelegt. By: Codex (GPT-5) |
|
||||
|
||||
---
|
||||
## Legende
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
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_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.
|
||||
|
||||
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 |
45
project-management/requirements/epics/EPIC_000010.md
Normal file
45
project-management/requirements/epics/EPIC_000010.md
Normal file
@ -0,0 +1,45 @@
|
||||
ID: EPIC_000010 | Version: 0.1.5 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# EPIC_000010: Update-Service v1 Migration (Major Release)
|
||||
|
||||
## Beschreibung
|
||||
Migration des Update-Clients auf den neuen v1 Update-Service mit verpflichtender Authentifizierung
|
||||
und Enrollment-Flow fuer Langzeit-Tokens. Diese Umstellung ist ein Major Release.
|
||||
|
||||
## Ziel / Business Value
|
||||
Sicheres, standardisiertes Update-Management mit verpflichtender Auth und nachvollziehbarem Status-Reporting.
|
||||
|
||||
## Mission Statement
|
||||
Stelle sicher, dass der Client die v1 Endpunkte nutzen kann, inkl. Enrollment und
|
||||
neuem Status-Schema.
|
||||
|
||||
## Business Value & Metriken
|
||||
- Security: Auth ist obligatorisch fuer alle Requests.
|
||||
- Erfolgsmetrik: 100% der Clients koennen per v1 manifest/artifact/status arbeiten.
|
||||
|
||||
## In-Scope (Kiddo Team)
|
||||
- Enrollment-Flow fuer Langzeit-Token (mit Pre-Shared Token).
|
||||
- Update-Client auf v1 Endpunkte umstellen.
|
||||
- Status-Payload auf v1 Schema umstellen.
|
||||
- Migration-Notiz/Docs fuer Client-Dev.
|
||||
|
||||
## Out-of-Scope
|
||||
- Betrieb/Hosting des Update-Services.
|
||||
- Ausgabe/Verwaltung von Pre-Shared Tokens auf Server-Seite.
|
||||
|
||||
## High-Level Akzeptanzkriterien
|
||||
- Auth ist Pflicht (Bearer Token) fuer Manifest, Artifact und Status.
|
||||
- Enrollment liefert Langzeit-Token, der lokal gespeichert wird.
|
||||
- v1 Endpunkte werden genutzt:
|
||||
- GET /v1/projects/{project_id}/manifest
|
||||
- GET /v1/projects/{project_id}/releases/{version}/artifact
|
||||
- POST /v1/projects/{project_id}/status
|
||||
|
||||
## Technische Constraints & Risiken
|
||||
- Major Release: Rollout-Strategie und Backward Compatibility klaeren.
|
||||
- Token-Handling und sichere lokale Speicherung.
|
||||
|
||||
## Zugeordnete User Stories
|
||||
- US_000034: Enrollment fuer Langzeit-Token
|
||||
- US_000035: v1 Update-Endpoints und Status-Schema
|
||||
17
project-management/requirements/stories/US_000034.md
Normal file
17
project-management/requirements/stories/US_000034.md
Normal file
@ -0,0 +1,17 @@
|
||||
ID: US_000034 | Version: 0.1.5 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000034: Enrollment fuer Langzeit-Token
|
||||
|
||||
Als Betreiber moechte ich, dass der Client einmalig einen Langzeit-Token per Enrollment bezieht,
|
||||
damit alle Update-Requests verpflichtend authentifiziert sind.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- Given ein Pre-Shared Token (von Admin bereitgestellt)
|
||||
- When der Client einen Enrollment-Request stellt
|
||||
- Then erhaelt er einen Langzeit-Token
|
||||
- And der Client speichert den Token lokal und nutzt ihn fuer alle Update-Requests
|
||||
- And Enrollment-Endpoint/Details werden per Spezifikation festgelegt
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000040: Enrollment-Flow implementieren (Details bei Story-Start)
|
||||
22
project-management/requirements/stories/US_000035.md
Normal file
22
project-management/requirements/stories/US_000035.md
Normal file
@ -0,0 +1,22 @@
|
||||
ID: US_000035 | Version: 0.1.5 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000035: v1 Update-Endpoints und Status-Schema
|
||||
|
||||
Als Betreiber moechte ich, dass der Client die neuen v1 Endpunkte fuer Manifest, Artefakt-Download
|
||||
und Status-Reporting nutzt, damit der Update-Service konsistent und sicher angesprochen wird.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- Given ein konfiguriertes project_id und Bearer Token
|
||||
- When der Client Updates prueft
|
||||
- Then nutzt er GET /v1/projects/{project_id}/manifest
|
||||
- And das Manifest enthaelt version, artifact_url, sha256, optional sig_url
|
||||
- When der Client ein Artefakt herunterlaedt
|
||||
- Then nutzt er GET /v1/projects/{project_id}/releases/{version}/artifact
|
||||
- When der Client Status meldet
|
||||
- Then nutzt er POST /v1/projects/{project_id}/status
|
||||
- And die Payload enthaelt project_id, version (SemVer), status (definierte Werte),
|
||||
optional client_id, duration_ms, error_code
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000041: v1 Endpunkte im Update-Client umstellen (Details bei Story-Start)
|
||||
21
project-management/requirements/tasks/TASK_000040.md
Normal file
21
project-management/requirements/tasks/TASK_000040.md
Normal file
@ -0,0 +1,21 @@
|
||||
ID: TASK_000040 | Version: 0.1.5 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000040: Enrollment-Flow implementieren
|
||||
|
||||
## Outcome
|
||||
Der Update-Client kann einmalig per Enrollment einen Langzeit-Token beziehen und lokal speichern.
|
||||
|
||||
## Story-Bezug
|
||||
US_000034
|
||||
|
||||
## Beschreibung
|
||||
- POST /v1/enroll mit { project_id, client_id, software_id, enroll_token }.
|
||||
- Langzeit-Token in lokaler Datei speichern (Pfad und Berechtigung definieren).
|
||||
- Token fuer alle weiteren Update-Requests nutzen.
|
||||
- Fehlercodes behandeln (unauthorized, invalid_payload, already_enrolled).
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- Enrollment laeuft mit Pre-Shared Token durch und liefert Langzeit-Token.
|
||||
- Token ist persistent und wird fuer Manifest/Artifact/Status verwendet.
|
||||
- Fehlerszenarien sind nachvollziehbar geloggt.
|
||||
22
project-management/requirements/tasks/TASK_000041.md
Normal file
22
project-management/requirements/tasks/TASK_000041.md
Normal file
@ -0,0 +1,22 @@
|
||||
ID: TASK_000041 | Version: 0.1.5 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000041: v1 Endpunkte im Update-Client umstellen
|
||||
|
||||
## Outcome
|
||||
Manifest, Artefakt-Download und Status-Reporting laufen ueber die v1 Endpunkte des Update-Services.
|
||||
|
||||
## Story-Bezug
|
||||
US_000035
|
||||
|
||||
## Beschreibung
|
||||
- Manifest: GET /v1/projects/{project_id}/manifest.
|
||||
- Artifact: GET /v1/projects/{project_id}/releases/{version}/artifact.
|
||||
- Status: POST /v1/projects/{project_id}/status mit neuem Payload.
|
||||
- Auth: Bearer Token ist Pflicht fuer alle Requests.
|
||||
- project_id ist fix: safe-kiddo-control.
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- Update-Client nutzt v1 Endpunkte fuer alle Requests.
|
||||
- Status-Payload entspricht dem neuen Schema.
|
||||
- Fehlermeldungen sind klar und retry-faehig.
|
||||
@ -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 <<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)"
|
||||
if [[ -z "${LATEST_BACKUP}" ]]; then
|
||||
|
||||
@ -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 <<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
|
||||
)
|
||||
if [[ -n "${UPDATE_TOKEN}" ]]; then
|
||||
@ -89,12 +99,12 @@ require_cmd tar
|
||||
require_cmd sha256sum
|
||||
require_cmd python3
|
||||
|
||||
log "Fetching update manifest from ${UPDATE_URL}..."
|
||||
log "Fetching update manifest from ${MANIFEST_URL}..."
|
||||
MANIFEST_FILE="$(mktemp)"
|
||||
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
|
||||
curl -fsS "${UPDATE_URL}" -o "${MANIFEST_FILE}"
|
||||
curl -fsS "${MANIFEST_URL}" -o "${MANIFEST_FILE}"
|
||||
fi
|
||||
|
||||
read_manifest() {
|
||||
|
||||
Reference in New Issue
Block a user