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:
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
|
||||
```
|
||||
Reference in New Issue
Block a user