mirror of
git://f0xx.org/ac/ac-docs
synced 2026-07-29 05:39:40 +03:00
initial
This commit is contained in:
548
specs/20100611_3_url_shortener.md
Normal file
548
specs/20100611_3_url_shortener.md
Normal file
@@ -0,0 +1,548 @@
|
||||
# URL shortener service — specification
|
||||
|
||||
<!-- doc-meta:start -->
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Author | Anton Afanasyeu |
|
||||
| Revision | R1 |
|
||||
| Creation date | 2026-06-11 |
|
||||
| Last modification date | 2026-06-11 |
|
||||
| Co-authored | |
|
||||
| Severity | medium |
|
||||
| State | scheduled |
|
||||
| Document type | spec |
|
||||
<!-- doc-meta:end -->
|
||||
|
||||
\newpage
|
||||
\newpage
|
||||
|
||||
---
|
||||
|
||||
|
||||
**Document type:** SPEC
|
||||
**Source draft:** [docs/drafts/20100611_3_url_shortener.txt](../drafts/20100611_3_url_shortener.txt)
|
||||
**Design review:** [docs/DRs/20100611_3_url_shortener.md](../DRs/20100611_3_url_shortener.md)
|
||||
**Status:** Post-alpha (next after alpha); priority may shift with BE capacity/load
|
||||
**Severity:** Medium
|
||||
**Public host:** `https://s.f0xx.org`
|
||||
**Repo placement:** `backend/url-shortener` submodule (git → `git://f0xx.org/androidcast_project/url-shortener`)
|
||||
**Scaffold:** [backend/url-shortener/](../../backend/url-shortener/) · **OpenAPI:** [openapi.yaml](../../backend/url-shortener/openapi.yaml)
|
||||
|
||||
**Documentation index:** [README.md](../README.md)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Table of contents
|
||||
|
||||
<!-- toc -->
|
||||
- [1. Purpose](#1-purpose)
|
||||
- [2. Scope](#2-scope)
|
||||
- [3. Requirements](#3-requirements)
|
||||
- [3.1 Must](#31-must)
|
||||
- [3.2 Should](#32-should)
|
||||
- [4. Architecture](#4-architecture)
|
||||
- [5. Authentication and trust](#5-authentication-and-trust)
|
||||
- [6. REST API](#6-rest-api)
|
||||
- [6.1 Shorten — GET](#61-shorten-get)
|
||||
- [6.2 Shorten — POST](#62-shorten-post)
|
||||
- [6.3 Error response](#63-error-response)
|
||||
- [6.4 Success response](#64-success-response)
|
||||
- [6.5 QR](#65-qr)
|
||||
- [7. Redirect and permalink behaviour](#7-redirect-and-permalink-behaviour)
|
||||
- [8. Slug algorithm](#8-slug-algorithm)
|
||||
- [9. Database schema](#9-database-schema)
|
||||
- [9.1 links](#91-links)
|
||||
- [9.2 bearer_tokens](#92-bearertokens)
|
||||
- [9.3 signer_keys](#93-signerkeys)
|
||||
- [9.4 audit_log](#94-auditlog)
|
||||
- [10. QR codes](#10-qr-codes)
|
||||
- [11. OpenAPI](#11-openapi)
|
||||
- [12. Deployment](#12-deployment)
|
||||
- [13. Security](#13-security)
|
||||
- [14. Observability and limits](#14-observability-and-limits)
|
||||
- [15. Tests](#15-tests)
|
||||
- [16. Out of scope (v1)](#16-out-of-scope-v1)
|
||||
- [17. Related docs](#17-related-docs)
|
||||
- [18. Routing (normative)](#18-routing-normative)
|
||||
- [19. Appendix A — curl cookbook](#19-appendix-a-curl-cookbook)
|
||||
- [20. Admin UI](#20-admin-ui)
|
||||
<!-- /toc -->
|
||||
|
||||
**Documentation index:** [README.md](README.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Provide **short, encoded URLs on demand** for long targets. The service is a **helper** for other apps and operators (Android Cast BE console, curl scripts, future mobile share flows) — not a user-facing product on its own.
|
||||
|
||||
**High-level flow:**
|
||||
|
||||
1. A client (any HTTP app; Android Cast not required) requests shortening of a long URL.
|
||||
2. The client authenticates with a **project bearer token** (and optionally a **signer** for elevated TTL).
|
||||
3. The service validates trust, creates or returns a cached short link, and responds with JSON.
|
||||
4. End users open `https://s.f0xx.org/{slug}` → HTTP redirect to the original URL (while TTL valid).
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope
|
||||
|
||||
| In scope (v1) | Out of scope (v1) |
|
||||
|---------------|-------------------|
|
||||
| Standalone opt-in service (not inside crash-reporter PHP app) | Alpha release blocker |
|
||||
| REST API + OpenAPI 3 document | Custom domains per tenant |
|
||||
| PHP + nginx + MariaDB stack | Click analytics dashboard |
|
||||
| Permalink reuse for same `(bearer, url)` | Link preview / OG scraper |
|
||||
| Time-limited and permanent links (signer-gated) | Federated multi-region |
|
||||
| QR code PNG for any short URL | Mobile SDK (HTTP only v1) |
|
||||
|
||||
**Scheduling:** Post-alpha by default. PO may reorder if BE load is low and another product track needs short links urgently (see DR §Estimates).
|
||||
|
||||
---
|
||||
|
||||
## 3. Requirements
|
||||
|
||||
### 3.1 Must
|
||||
|
||||
| ID | Requirement |
|
||||
|----|-------------|
|
||||
| R1 | Standalone service; development **separate** from main BE crash/tickets app |
|
||||
| R2 | REST API documented with **OpenAPI 3** (`openapi.yaml` in repo) |
|
||||
| R3 | Stack: **PHP 8.x**, **nginx**, **MariaDB**; OSS dependencies only (MIT/BSD/Apache) |
|
||||
| R4 | Project path: `backend/url-shortener` submodule |
|
||||
| R5 | **QR code** generation (PNG) for shortened URLs |
|
||||
| R6 | **Permalink** semantics: same bearer + same normalized URL → same slug while record valid |
|
||||
| R7 | **TTL**: temporary links expire; `ttl=0` permanent only for trusted signers |
|
||||
| R8 | JSON request/response contract (§6) |
|
||||
| R9 | Reverse lookup: `GET /{slug}` → `302` to stored origin (or `410` when expired) |
|
||||
|
||||
### 3.2 Should
|
||||
|
||||
| ID | Requirement |
|
||||
|----|-------------|
|
||||
| S1 | Support **GET** (query) and **POST** (JSON body) shorten endpoints |
|
||||
| S2 | Rate limits per bearer token |
|
||||
| S3 | Audit log table for create/access/deny |
|
||||
| S4 | Admin token mint/revoke via hub **Short links** card + shared `ac_crash_sess` RBAC — [§20](#20-admin-ui) |
|
||||
| S5 | Unit tests (PHP) + curl-based smoke script in `scripts/` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
```text
|
||||
Client (curl, BE job, future app)
|
||||
│ HTTPS shorten API
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ FE — Gentoo (s.f0xx.org TLS) │
|
||||
│ nginx reverse_proxy │
|
||||
└──────────────┬──────────────────────┘
|
||||
│ proxy_pass → BE :80
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ BE — Alpine │
|
||||
│ nginx → php-fpm (url-shortener) │
|
||||
│ MariaDB (url_shortener schema) │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
Browser / QR scan
|
||||
│ GET https://s.f0xx.org/{slug}
|
||||
▼
|
||||
same path → lookup → 302 Location: origin
|
||||
```
|
||||
|
||||
- **Not** mounted under `/app/androidcast_project/crashes/` — dedicated vhost `s.f0xx.org` (DR §Deployment).
|
||||
- **Opt-in deploy:** absent from default BE sync until operator enables submodule + nginx fragment.
|
||||
|
||||
---
|
||||
|
||||
## 5. Authentication and trust
|
||||
|
||||
Normative decisions: [DR §Authentication and trust](../DRs/20100611_3_url_shortener.md#authentication-and-trust).
|
||||
|
||||
| Concept | Spec |
|
||||
|---------|------|
|
||||
| **bearer** | Project-scoped API token (`varchar(128)`), stored hashed in DB |
|
||||
| **signer** | Optional second secret proving **elevated** rights (permanent TTL, high quotas) |
|
||||
| **demo token** | Pre-seeded read-only or low-quota token for smoke tests |
|
||||
| **App-wide session** | Human operators mint/revoke bearer tokens via BE console using existing `ac_crash_sess` RBAC — not used for machine `curl` shorten calls |
|
||||
|
||||
**Trust checks (shorten):**
|
||||
|
||||
1. `bearer` required → `401` if missing/unknown/revoked.
|
||||
2. Lookup existing row for `(bearer_id, normalized_origin)`:
|
||||
- If found and not expired → return cached short URL; `ttl` in response = **remaining seconds**.
|
||||
- If found but expired → `409` / error code `TTL_EXPIRED` (do not extend without new shorten).
|
||||
3. If not found → require **whitelist/trusted** bearer for create.
|
||||
4. If `ttl=0` (permanent) → require valid **signer** with `can_permanent` capability.
|
||||
5. If `ttl>0` → bearer must have `can_temporary` (default for trusted tokens).
|
||||
|
||||
---
|
||||
|
||||
## 6. REST API
|
||||
|
||||
Base: `https://s.f0xx.org/api/v1`
|
||||
|
||||
### 6.1 Shorten — GET
|
||||
|
||||
```http
|
||||
GET /api/v1/shorten?url={URL_ENCODED}&bearer={TOKEN}&signer={SIGNER}&ttl={TTL}
|
||||
```
|
||||
|
||||
| Param | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `url` | yes | URL-encoded long URL (max 2048 chars after decode) |
|
||||
| `bearer` | yes | API token |
|
||||
| `signer` | if `ttl=0` | Signer secret for permanent links |
|
||||
| `ttl` | no | Seconds until expiry; `0` = permanent (signer required); default `86400` |
|
||||
|
||||
### 6.2 Shorten — POST
|
||||
|
||||
```http
|
||||
POST /api/v1/shorten
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"url": "https://defender.net/very/long/link#with_anchors¶m1=value1",
|
||||
"bearer": "4abef433d23ab5x&&83^c6",
|
||||
"signer": "optional-signer-key",
|
||||
"ttl": 3600
|
||||
}
|
||||
```
|
||||
|
||||
Prefer POST when URL length or sensitive query strings make GET impractical.
|
||||
|
||||
### 6.3 Error response
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "ACCESS_DENIED",
|
||||
"url": "https://defender.net/...",
|
||||
"bearer": "4abef…",
|
||||
"desc": "access denied"
|
||||
}
|
||||
```
|
||||
|
||||
| `code` | HTTP | Meaning |
|
||||
|--------|------|---------|
|
||||
| `0` | 200 | Success (success body below) |
|
||||
| `ACCESS_DENIED` | 403 | Bearer not trusted / revoked |
|
||||
| `MISSING_BEARER` | 401 | No bearer |
|
||||
| `INVALID_URL` | 400 | Malformed or disallowed scheme |
|
||||
| `TTL_EXPIRED` | 409 | Cached link expired |
|
||||
| `SIGNER_REQUIRED` | 403 | `ttl=0` without valid signer |
|
||||
| `RATE_LIMITED` | 429 | Quota exceeded |
|
||||
| `INTERNAL` | 500 | Server error |
|
||||
|
||||
### 6.4 Success response
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "0",
|
||||
"url": "https://defender.net/very/long/link#with_anchors¶m1=value1",
|
||||
"u": "https://s.f0xx.org/a1b2c3d4e5f6",
|
||||
"bearer": "4abef433d23ab5x&&83^c6",
|
||||
"ttl": "3542"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `url` | Original URL (normalized canonical form) |
|
||||
| `u` | Short URL |
|
||||
| `bearer` | Echo of bearer (truncated in logs only) |
|
||||
| `ttl` | Remaining seconds; `"0"` if permanent |
|
||||
|
||||
Optional fields (v1.1 suggestion in DR): `qr` — URL to QR PNG.
|
||||
|
||||
### 6.5 QR
|
||||
|
||||
```http
|
||||
GET /api/v1/qr/{slug}.png?size=256
|
||||
```
|
||||
|
||||
Returns `image/png` encoding `https://s.f0xx.org/{slug}`. `404` if slug unknown; `410` if expired.
|
||||
|
||||
---
|
||||
|
||||
## 7. Redirect and permalink behaviour
|
||||
|
||||
| Request | Behaviour |
|
||||
|---------|-----------|
|
||||
| `GET /{slug}` | `302 Found` → `Location: origin`; update `accessed_at` |
|
||||
| Expired slug | `410 Gone` HTML or JSON if `Accept: application/json` |
|
||||
| Unknown slug | `404 Not Found` |
|
||||
| `HEAD /{slug}` | Same as GET without body |
|
||||
|
||||
**Permalink:** For a given `(bearer, normalized_origin)`, the **same slug** is returned on repeat shorten while the row is valid. New slug only after expiry + delete or explicit admin purge.
|
||||
|
||||
**URL normalization (before hash/store):**
|
||||
|
||||
- Trim whitespace
|
||||
- Lowercase scheme and host
|
||||
- Preserve path, query, fragment
|
||||
- Reject non-`http`/`https` schemes
|
||||
|
||||
---
|
||||
|
||||
## 8. Slug algorithm
|
||||
|
||||
**Normative (per DR):** SHA-256 truncated — **not** Java `String.hashCode` or character interleaving from the draft.
|
||||
|
||||
```
|
||||
normalized = normalize_url(url)
|
||||
material = bearer_id + "\x1f" + normalized
|
||||
digest = SHA256(material)
|
||||
slug = lowercase_hex(digest[0:6]) // 12 hex chars → ~48 bits
|
||||
short_url = "https://s.f0xx.org/" + slug
|
||||
```
|
||||
|
||||
**Collision handling:** On insert conflict on `slug`, append `1`…`f` suffix (max 4 chars) or re-hash with salt counter — see DR §Slug algorithm.
|
||||
|
||||
**Reverse lookup:** Primary key lookup on `slug` column (indexed unique). No recomputation at redirect time.
|
||||
|
||||
_Draft note preserved in DR §Slug algorithm — interleave + Java hash rejected for production._
|
||||
|
||||
---
|
||||
|
||||
## 9. Database schema
|
||||
|
||||
Database: `url_shortener` (separate schema or DB — DR §Data store).
|
||||
|
||||
### 9.1 `links`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | `BIGINT UNSIGNED AUTO_INCREMENT` | PK |
|
||||
| `bearer_id` | `INT UNSIGNED` | FK → `bearer_tokens.id` |
|
||||
| `signer_id` | `INT UNSIGNED NULL` | FK → `signer_keys.id` if used |
|
||||
| `slug` | `VARCHAR(16)` | Unique, indexed |
|
||||
| `origin` | `TEXT` | Normalized original URL |
|
||||
| `origin_hash` | `CHAR(64)` | SHA-256 of origin (dedup aid) |
|
||||
| `ttl_seconds` | `INT UNSIGNED` | `0` = permanent |
|
||||
| `expires_at` | `DATETIME NULL` | NULL when permanent |
|
||||
| `created_at` | `DATETIME` | |
|
||||
| `updated_at` | `DATETIME` | |
|
||||
| `accessed_at` | `DATETIME NULL` | Last redirect |
|
||||
| `access_count` | `BIGINT UNSIGNED` | Default 0 |
|
||||
|
||||
Unique index: `(bearer_id, origin_hash)` where `expires_at IS NULL OR expires_at > NOW()` — implementation via application check + partial unique strategy per MariaDB version (DR).
|
||||
|
||||
### 9.2 `bearer_tokens`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | `INT UNSIGNED AUTO_INCREMENT` | |
|
||||
| `token_hash` | `CHAR(64)` | SHA-256 of bearer secret |
|
||||
| `label` | `VARCHAR(128)` | Operator label |
|
||||
| `trusted` | `TINYINT(1)` | Whitelist flag |
|
||||
| `can_temporary` | `TINYINT(1)` | Default 1 for trusted |
|
||||
| `can_permanent` | `TINYINT(1)` | Default 0; needs signer anyway |
|
||||
| `rate_limit_per_hour` | `INT UNSIGNED` | Default 1000 |
|
||||
| `revoked_at` | `DATETIME NULL` | |
|
||||
| `created_at` | `DATETIME` | |
|
||||
|
||||
### 9.3 `signer_keys`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | `INT UNSIGNED AUTO_INCREMENT` | |
|
||||
| `key_hash` | `CHAR(64)` | SHA-256 of signer secret |
|
||||
| `label` | `VARCHAR(128)` | |
|
||||
| `can_permanent` | `TINYINT(1)` | |
|
||||
| `revoked_at` | `DATETIME NULL` | |
|
||||
|
||||
### 9.4 `audit_log`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | `BIGINT UNSIGNED AUTO_INCREMENT` | |
|
||||
| `event` | `VARCHAR(32)` | `shorten`, `redirect`, `deny`, … |
|
||||
| `bearer_id` | `INT UNSIGNED NULL` | |
|
||||
| `slug` | `VARCHAR(16) NULL` | |
|
||||
| `ip` | `VARCHAR(45)` | |
|
||||
| `detail` | `TEXT` | |
|
||||
| `created_at` | `DATETIME` | |
|
||||
|
||||
---
|
||||
|
||||
## 10. QR codes
|
||||
|
||||
- Library: **endroid/qr-code** (MIT) or pure-PHP equivalent — license entry required in BE licenses file before merge.
|
||||
- Default size: 256×256 PNG, ECC level M.
|
||||
- Content: full short URL string (`https://s.f0xx.org/{slug}`).
|
||||
- Cache: optional filesystem cache keyed by slug+size (DR suggestion).
|
||||
|
||||
---
|
||||
|
||||
## 11. OpenAPI
|
||||
|
||||
Ship [backend/url-shortener/openapi.yaml](../../backend/url-shortener/openapi.yaml) covering:
|
||||
|
||||
- `POST /api/v1/shorten`
|
||||
- `GET /api/v1/shorten` (documented; mark `POST` preferred)
|
||||
- `GET /api/v1/qr/{slug}.png`
|
||||
- `GET /{slug}` redirect (document as `GET /r/{slug}` if internal router separates API — DR §Routing)
|
||||
|
||||
Serve docs at `GET /api/v1/docs` (Swagger UI static bundle or Redoc) — opt-in on staging only.
|
||||
|
||||
---
|
||||
|
||||
## 12. Deployment
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| Submodule path | `backend/url-shortener` |
|
||||
| Remote | `git://f0xx.org/androidcast_project/url-shortener` (live submodule) |
|
||||
| FE TLS | `/etc/letsencrypt/live/s.f0xx.org/` (live on FE) |
|
||||
| BE docroot | `/var/www/localhost/htdocs/apps/s/` |
|
||||
| nginx | [deploy/nginx.fe-s.f0xx.org.snippet](../../backend/url-shortener/deploy/nginx.fe-s.f0xx.org.snippet), [deploy/nginx.be-url-shortener.fragment](../../backend/url-shortener/deploy/nginx.be-url-shortener.fragment) |
|
||||
| PHP-FPM | Same pool as other BE apps or dedicated pool if load warrants |
|
||||
| MariaDB | Schema `url_shortener` |
|
||||
| Enable | Operator copies submodule, runs migration, includes nginx fragment, reload |
|
||||
|
||||
**Not required** for alpha BE validation script `validate_be_services.sh` until service is enabled.
|
||||
|
||||
---
|
||||
|
||||
## 13. Security
|
||||
|
||||
| Topic | Rule |
|
||||
|-------|------|
|
||||
| Secrets at rest | Store only hashes of bearer/signer |
|
||||
| TLS | Required on public FE |
|
||||
| SSRF | Block shorten targets to `127.0.0.0/8`, `10.0.0.0/8`, `169.254.0.0/16`, `metadata` hosts |
|
||||
| Open redirect | Only stored origins issued by trusted bearers |
|
||||
| Logging | Never log full bearer/signer; truncate to 6 chars |
|
||||
| CSRF | N/A for token-based API; admin UI uses session + CSRF token |
|
||||
|
||||
Optional HMAC request signing for GET — DR §Suggestions.
|
||||
|
||||
---
|
||||
|
||||
## 14. Observability and limits
|
||||
|
||||
| Limit | Default |
|
||||
|-------|---------|
|
||||
| Max URL length | 2048 |
|
||||
| Max slug length | 16 |
|
||||
| Rate limit | 1000 shorten/hour per bearer |
|
||||
| Audit retention | 90 days (cron purge script) |
|
||||
|
||||
Metrics (suggestion): redirect count, 4xx/5xx rate — post-v1 Prometheus or nginx logs.
|
||||
|
||||
---
|
||||
|
||||
## 15. Tests
|
||||
|
||||
| Layer | Location |
|
||||
|-------|----------|
|
||||
| PHP unit | `backend/url-shortener/tests/` — normalize, slug, TTL math, auth |
|
||||
| Smoke | `backend/url-shortener/scripts/smoke_shorten.sh` |
|
||||
| BE validation | Optional line in `validate_be_services.sh` when deployed |
|
||||
|
||||
---
|
||||
|
||||
## 16. Out of scope (v1)
|
||||
|
||||
- Link editing after creation
|
||||
- Custom slugs chosen by user
|
||||
- Analytics UI
|
||||
- Integration inside Android APK (use HTTP from BE or scripts first)
|
||||
|
||||
---
|
||||
|
||||
## 17. Related docs
|
||||
|
||||
- [DR — design review](../DRs/20100611_3_url_shortener.md)
|
||||
- [backend/url-shortener/README.md](../../backend/url-shortener/README.md) — scaffold + deploy
|
||||
- [INFRA.md](../INFRA.md) — FE → BE proxy
|
||||
- [20260610_SERVICES.md](../20260610_SERVICES.md) — service catalog (preview row)
|
||||
- [ROADMAP.md](../ROADMAP.md) — post-alpha track
|
||||
|
||||
---
|
||||
|
||||
## 18. Routing (normative)
|
||||
|
||||
Single vhost `s.f0xx.org`. **API and redirect share the host**; paths are disjoint:
|
||||
|
||||
```text
|
||||
https://s.f0xx.org
|
||||
/api/v1/health → JSON health
|
||||
/api/v1/shorten → shorten (GET query or POST JSON)
|
||||
/api/v1/qr/{slug}.png → QR PNG
|
||||
/{slug} → 302 redirect (regex: [a-f0-9]{6,16})
|
||||
```
|
||||
|
||||
```text
|
||||
Client nginx (FE TLS) nginx (BE) PHP
|
||||
│ POST /api/v1/shorten proxy_pass :80 /api/* → index.php
|
||||
├──────────────────────────►──────────────────────────►──────────────────► shorten handler
|
||||
│
|
||||
│ GET /{slug}
|
||||
├──────────────────────────►──────────────────────────► slug regex ──────► redirect handler
|
||||
```
|
||||
|
||||
FE: full URI proxied to BE `:80` with `Host: s.f0xx.org` (see deploy snippet).
|
||||
BE: `try_files` → `public/index.php` for `/api/` and slug paths.
|
||||
|
||||
---
|
||||
|
||||
## 19. Appendix A — curl cookbook
|
||||
|
||||
Runnable script: [backend/url-shortener/scripts/demo_curl.sh](../../backend/url-shortener/scripts/demo_curl.sh)
|
||||
|
||||
```bash
|
||||
export BASE=https://s.f0xx.org
|
||||
export BEARER=your-trusted-token
|
||||
export TARGET='https://defender.net/very/long/link?demo=1'
|
||||
|
||||
# 1) Health
|
||||
curl -fsS "$BASE/api/v1/health"
|
||||
|
||||
# 2) Shorten (POST)
|
||||
curl -fsS -X POST "$BASE/api/v1/shorten" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"url":"'"$TARGET"'","bearer":"'"$BEARER"'","ttl":3600}'
|
||||
|
||||
# 3) Follow redirect (HEAD)
|
||||
SHORT=https://s.f0xx.org/a1b2c3d4e5f6 # from .u in response
|
||||
curl -sSI "$SHORT" | grep -E '^(HTTP|Location:)'
|
||||
|
||||
# 4) QR PNG
|
||||
curl -fsS -o /tmp/qr.png "$BASE/api/v1/qr/a1b2c3d4e5f6.png?size=256"
|
||||
```
|
||||
|
||||
Deploy smoke: [scripts/smoke_shorten.sh](../../backend/url-shortener/scripts/smoke_shorten.sh)
|
||||
|
||||
---
|
||||
|
||||
## 20. Admin UI
|
||||
|
||||
**Decision (DR rev. 2):** operator token management lives in the **hub** as a **Short links** nav card (same theme as Remote access / Builder), not a separate skin.
|
||||
|
||||
| Action | Auth |
|
||||
|--------|------|
|
||||
| Mint / revoke bearer tokens | `ac_crash_sess` + RBAC `admin` / `operator` |
|
||||
| List links per bearer | same |
|
||||
| Purge expired | `admin` |
|
||||
|
||||
Machine clients (`curl`, BE jobs) use **bearer tokens only** — no session cookie on `/api/v1/shorten`.
|
||||
659
specs/20100611_3_url_shortener.pdf
Normal file
659
specs/20100611_3_url_shortener.pdf
Normal file
@@ -0,0 +1,659 @@
|
||||
%PDF-1.4
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 35 0 R /F4 36 0 R /F5 37 0 R /F6 39 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 762.0394 84.37304 773.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 751.0394 77.26104 762.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 740.0394 104.373 751.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 729.6019 88.44754 739.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 719.6019 95.54254 729.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 709.0394 97.26104 720.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 698.0394 138.629 709.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 687.0394 91.03704 698.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 676.6019 125.55 686.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 666.6019 130.5525 676.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 656.6019 121.7925 666.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 646.6019 133.47 656.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 636.6019 83.44504 646.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 626.0394 174.181 637.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 615.0394 105.709 626.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 604.0394 119.053 615.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 593.6019 87.19504 603.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 583.6019 120.135 593.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 573.6019 112.2075 583.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 563.6019 102.63 573.6019 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 553.0394 94.59704 564.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 542.0394 91.49304 553.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 531.0394 101.709 542.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 520.0394 87.92504 531.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 509.0394 141.269 520.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 498.0394 78.58904 509.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 487.0394 120.389 498.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 476.0394 105.717 487.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
32 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 465.0394 129.269 476.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
33 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 454.0394 161.741 465.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
34 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 443.0394 91.92504 454.0394 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
35 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
36 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
37 0 obj
|
||||
<<
|
||||
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
38 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 387.5218 95.58819 399.1978 103.9882 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
39 0 obj
|
||||
<<
|
||||
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
40 0 obj
|
||||
<<
|
||||
/Contents 85 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
41 0 obj
|
||||
<<
|
||||
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
|
||||
14 0 R 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R
|
||||
24 0 R 25 0 R 26 0 R 27 0 R 28 0 R 29 0 R 30 0 R 31 0 R 32 0 R 33 0 R
|
||||
34 0 R ] /Contents 86 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0
|
||||
/Trans <<
|
||||
|
||||
>> /Type /Page
|
||||
>>
|
||||
endobj
|
||||
42 0 obj
|
||||
<<
|
||||
/Contents 87 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
43 0 obj
|
||||
<<
|
||||
/Contents 88 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
44 0 obj
|
||||
<<
|
||||
/Annots [ 38 0 R ] /Contents 89 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0
|
||||
/Trans <<
|
||||
|
||||
>> /Type /Page
|
||||
>>
|
||||
endobj
|
||||
45 0 obj
|
||||
<<
|
||||
/Contents 90 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
46 0 obj
|
||||
<<
|
||||
/Contents 91 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
47 0 obj
|
||||
<<
|
||||
/Contents 92 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
48 0 obj
|
||||
<<
|
||||
/Contents 93 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
49 0 obj
|
||||
<<
|
||||
/Contents 94 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 84 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
50 0 obj
|
||||
<<
|
||||
/Outlines 52 0 R /PageMode /UseNone /Pages 84 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
51 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260623115607+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115607+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (URL shortener service \204 specification) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
52 0 obj
|
||||
<<
|
||||
/Count 34 /First 53 0 R /Last 83 0 R /Type /Outlines
|
||||
>>
|
||||
endobj
|
||||
53 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 54 0 R /Parent 52 0 R /Title (1. Purpose)
|
||||
>>
|
||||
endobj
|
||||
54 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 55 0 R /Parent 52 0 R /Prev 53 0 R /Title (2. Scope)
|
||||
>>
|
||||
endobj
|
||||
55 0 obj
|
||||
<<
|
||||
/Count 2 /Dest [ 40 0 R /Fit ] /First 56 0 R /Last 57 0 R /Next 58 0 R /Parent 52 0 R
|
||||
/Prev 54 0 R /Title (3. Requirements)
|
||||
>>
|
||||
endobj
|
||||
56 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 57 0 R /Parent 55 0 R /Title (3.1 Must)
|
||||
>>
|
||||
endobj
|
||||
57 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Parent 55 0 R /Prev 56 0 R /Title (3.2 Should)
|
||||
>>
|
||||
endobj
|
||||
58 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 59 0 R /Parent 52 0 R /Prev 55 0 R /Title (4. Architecture)
|
||||
>>
|
||||
endobj
|
||||
59 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 60 0 R /Parent 52 0 R /Prev 58 0 R /Title (5. Authentication and trust)
|
||||
>>
|
||||
endobj
|
||||
60 0 obj
|
||||
<<
|
||||
/Count 5 /Dest [ 40 0 R /Fit ] /First 61 0 R /Last 65 0 R /Next 66 0 R /Parent 52 0 R
|
||||
/Prev 59 0 R /Title (6. REST API)
|
||||
>>
|
||||
endobj
|
||||
61 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 62 0 R /Parent 60 0 R /Title (6.1 Shorten \204 GET)
|
||||
>>
|
||||
endobj
|
||||
62 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 63 0 R /Parent 60 0 R /Prev 61 0 R /Title (6.2 Shorten \204 POST)
|
||||
>>
|
||||
endobj
|
||||
63 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 64 0 R /Parent 60 0 R /Prev 62 0 R /Title (6.3 Error response)
|
||||
>>
|
||||
endobj
|
||||
64 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 65 0 R /Parent 60 0 R /Prev 63 0 R /Title (6.4 Success response)
|
||||
>>
|
||||
endobj
|
||||
65 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Parent 60 0 R /Prev 64 0 R /Title (6.5 QR)
|
||||
>>
|
||||
endobj
|
||||
66 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 67 0 R /Parent 52 0 R /Prev 60 0 R /Title (7. Redirect and permalink behaviour)
|
||||
>>
|
||||
endobj
|
||||
67 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 68 0 R /Parent 52 0 R /Prev 66 0 R /Title (8. Slug algorithm)
|
||||
>>
|
||||
endobj
|
||||
68 0 obj
|
||||
<<
|
||||
/Count 4 /Dest [ 40 0 R /Fit ] /First 69 0 R /Last 72 0 R /Next 73 0 R /Parent 52 0 R
|
||||
/Prev 67 0 R /Title (9. Database schema)
|
||||
>>
|
||||
endobj
|
||||
69 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 70 0 R /Parent 68 0 R /Title (9.1 links)
|
||||
>>
|
||||
endobj
|
||||
70 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 71 0 R /Parent 68 0 R /Prev 69 0 R /Title (9.2 bearer_tokens)
|
||||
>>
|
||||
endobj
|
||||
71 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 72 0 R /Parent 68 0 R /Prev 70 0 R /Title (9.3 signer_keys)
|
||||
>>
|
||||
endobj
|
||||
72 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Parent 68 0 R /Prev 71 0 R /Title (9.4 audit_log)
|
||||
>>
|
||||
endobj
|
||||
73 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 74 0 R /Parent 52 0 R /Prev 68 0 R /Title (10. QR codes)
|
||||
>>
|
||||
endobj
|
||||
74 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 75 0 R /Parent 52 0 R /Prev 73 0 R /Title (11. OpenAPI)
|
||||
>>
|
||||
endobj
|
||||
75 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 76 0 R /Parent 52 0 R /Prev 74 0 R /Title (12. Deployment)
|
||||
>>
|
||||
endobj
|
||||
76 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 77 0 R /Parent 52 0 R /Prev 75 0 R /Title (13. Security)
|
||||
>>
|
||||
endobj
|
||||
77 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 78 0 R /Parent 52 0 R /Prev 76 0 R /Title (14. Observability and limits)
|
||||
>>
|
||||
endobj
|
||||
78 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 79 0 R /Parent 52 0 R /Prev 77 0 R /Title (15. Tests)
|
||||
>>
|
||||
endobj
|
||||
79 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 80 0 R /Parent 52 0 R /Prev 78 0 R /Title (16. Out of scope \(v1\))
|
||||
>>
|
||||
endobj
|
||||
80 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 81 0 R /Parent 52 0 R /Prev 79 0 R /Title (17. Related docs)
|
||||
>>
|
||||
endobj
|
||||
81 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 82 0 R /Parent 52 0 R /Prev 80 0 R /Title (18. Routing \(normative\))
|
||||
>>
|
||||
endobj
|
||||
82 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Next 83 0 R /Parent 52 0 R /Prev 81 0 R /Title (19. Appendix A \204 curl cookbook)
|
||||
>>
|
||||
endobj
|
||||
83 0 obj
|
||||
<<
|
||||
/Dest [ 40 0 R /Fit ] /Parent 52 0 R /Prev 82 0 R /Title (20. Admin UI)
|
||||
>>
|
||||
endobj
|
||||
84 0 obj
|
||||
<<
|
||||
/Count 10 /Kids [ 40 0 R 41 0 R 42 0 R 43 0 R 44 0 R 45 0 R 46 0 R 47 0 R 48 0 R 49 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
85 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 856
|
||||
>>
|
||||
stream
|
||||
Gat=*9lldX&A@sBlnQW3MH6?g9[fdn*?U`;p65qpb6]$g0&SS^pQ]\O+13hJ(gDWgp\*jV.g^d\#L!-9%.H-)l[&bF&?Gj8&J31E/suR)i9*cWM=Mo,TX?,2qnh@Z0VKQJOos[WU=o^D-1_:,p,oUM\.>>XLi!D%-s[+u>QAl.OE*E(/E,U?isbLF1b27/1<<2`(+fqW0r?,OQpnB>(1@fOaX%(n&7!&YNONpMluP_'66^3eP+o,k\ntM(-ZURQSU,&ZFWIepSb&It\+"LE8b6C0XuA#m(Wa^U^;#jgH[`Q-PkR_6RP7O(;O\>aB2,kl.E;)A^$[6X`hd8?#t(Sk>7r[nX"2BXSn\b1LZX\]:dDLl;"QTNIh&_Ic)].K'rQ?Q=[r&TQ?C0^/+\Pp?W=Itmli$L]=L#DWj4'0cnEl:4u2Hcd7cl`6r6kG0`MX7>LR"uU0T5?/;8BP!b*tDc*jj!1ghnJ:a\]#^s+?&_Yh;fO&mCt6[WWg5FT&0Deqh^KqH2@4]:rd)f@H1m[:9fMsdK+TQbb`7[]]CAbY1*h.j;+ZS4$T:7(iPJ)CCJ7iJ`71#D\I)OB&n#1T[&h>LX[+2*iJGr&T&I%63USi\jRBH-\*KVKQV(PBHcGiF,@@[>bkO7CfWZ)$:*`ATVG!QE)kJEkP[mY5e5p4rU[:5HrtOS5^&Bck+EgC4#9+80]&(qJ6Ubf_0`*2'EOg6gN[EeMM>(7IUl-?D+tjq+XlILG5g(u9f-)PL=?<?jm>+_h-u+a?@U>?6GCTm%U$iV"qT=s&c1cP.k'd)2kgM&(nN6+?6OQTe9=#A,1k,\KMdhZG3^7.eXVjorW026W8O0_krP3e[h~>endstream
|
||||
endobj
|
||||
86 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1034
|
||||
>>
|
||||
stream
|
||||
Gatn'bAQ&g&4Q?hMHM930Hrs!\oSI].+#h+XR6-,ZGbCs,uGg7Vtp4MA1)%3:$I0>//d%!g_l82=/HPe7`Tr1!.;oIr/q+mOP^-$KL>_ZVYS?i#sDP8,8-nO^`c8^&qh.CqsZEK;AM_FO>)TF,R'?!/E7H:q?3E^T.i*P]Kl``0Gk`7Vg5PI"fPWR<GP$=bo,TW+TF:&YAeiH*sWnGLO2.;FGu<AF@ortlV8>VJE<1P/0hAI<TaDm;d[n5^pU]<V&#&#T%WXMb(5]=&YBJ=Do\On"]5!A)s]bd^<$d!)qEQ*"$?;!m!^bNSA7OZ)eh`THgNN@E>JF\8'(,c#"Mj_KpAZlU924GVu5T3Q];nqh/ndsAH'i)RiKb&DqnimEB*A)$Ob`DQGD*dm[tctXb&];0qU%&A44.CKJ:q[C!a_=Wcb:d1&%f$_kJeW@E^GU6Jj7:\@QkA1@Aoq,X_+(YkFB+>_nrt9i9)W$S/J2G#J"e5n=EQkt/,^ebpDDHEFQ]S&U"#0OOI2^dV[i<FH#tE[.%4s7e!(*rk(BaKRQ2"5qN3UXV5kiNO,%kbV9`dCMk>HqOa]aE<CR[c5+l5jV\TLSCuJ?.NcXJ<-WfcPI-trTH0ji=\*DB/\.Sc8s?6[6f8D2_Nh/$dmMi@5*-?kRY2,J"b83/k"B&M0X%?qm0W("p;2s&UIN[A<$5dlIN8'0$]g\o3i>m=>#OGeXu0%BY.li*T!j@n=fqBV`T3Gk+Emff(/%WoBO2M$7]&e,Tug3hf&q?[Hal1f,/k8YhN?l'2C!4]_+:BNGd2'P-<qbR-`'>/qt:&O4pc+j.M-R.sqa5hkY!t$U5X=&4b1a-35.&DdS%_-p:gcb+aDu0nkt7=3;R'63t]I*=P0*\cl91;LAZacle,n?%u"reC*]CZd>MIp'ES>Tn`<0Z$F7#dTaMKjqjAgPN#n^"*RUF0R_R3Xa&+uh%hj`J.eJcaK*ZZ_C82gG^8(;J')4iikp6iIg$O?N$Mht!fKT;BfZHBb2=6AHNo(c58e\!KE~>endstream
|
||||
endobj
|
||||
87 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 140
|
||||
>>
|
||||
stream
|
||||
Gar'`]aDVA&B+*fTAj4X`?fRI\P"GdAL$O\#RWm*IK./0>U9b60^r:$Y,=E)$_3dH.MVD#as#e(/oDa=2+r)O^V;GfW,6,V;^^n2rk5*EC76*HKHdgU0,tT,)NXY45AfCtfFMZc*Cg~>endstream
|
||||
endobj
|
||||
88 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 140
|
||||
>>
|
||||
stream
|
||||
Gar'`YmS?5&;9"kTAhN!`?fT9b?\:;.iVpS5Yr"ikkc/CiMs+9!s@G<@6O#R>i=W62kc&B:^RNDF*7@`-qPm%a8WcSTt6XP9T?-Sg@m_L:r^$YF3%8(pd1>2d;Zrepj!M`_u=?S*Cp~>endstream
|
||||
endobj
|
||||
89 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3562
|
||||
>>
|
||||
stream
|
||||
Gau0F=``=W&q9SYkXTo-;Zq>,[1?%!a^iC/4-J`#<A'/^,fUP7nF3\eqqnm!6_Z"!P-a8<fG("H7\eB0I16gNnV:ZT3ZETF1Dp-eJ5DUbJZ+_=ZTG[fSc(Yq`Cs9qFAnB'0J$M(Z>/=lcFtBk?ttj%9_`mfP0$`BPr+kFJB6aE*NGoC$B9*"'<PYa_jtneJcq7GOBnEplpkYi2JWHELYL/.B=$KKFl+,H0XRs9K+44=1iaGCOhFEuoXin8a4X9OVD=9FSWHA'0oc+OGg3":"Y>h"f;+1]](umaZ.N7a&@#l/]c9!%B$t/I`pI3-VS9s)M4LI4JRo?S:+gBJ>h^qS"me)IoH]>;qe1HX&]2WLOdE_s`dWPbm4AurnWGbW\cp+aD$Pj95.CEVY:?IIGKQMF0]QOAhjE`+Ri^8uj`KZKU^Q($@#`Gm0VEY[%GC>68](7;4H?87r;%o5B[t!Y8l,3*KS:C4hfbAHUg<+7D!-WYmMM_fEZ9H"n&4I"_qigT(_\DQ.:m2gG\P?7:,RJ"X4s>Z>*OV&.6TGp"#o52le$4J(e4E.F-9R^k3"mt8E`-1LLW!(gij:aGLTu0QrDfhf2r:th1mPkIe\43K&c$;kXf^d`p^/@fod%:-I')+eg4&<e<UF#SB5Bd"BdL&]ig&,R?FW_X_!)2&PKT:-;[:].;bYa>*;>a]k)dJTp-<-Qjq4F[J_F?Cc?)^2Hr:FGFM.mO@CCS=ndC/e7qsXlphdN8/i.jp0:W/PUqhIJ-?F,R1'$gmUG)T\86f\U)lHj*eDXi'dRt0@S&,eCs=)7dBeTcd>PiDMdJ8)4?V(Z%StY"[>,oS.r[pb7WuYD(m"K8INm`,*m#tBo-!0Z]-n[VEps2slpP0I`##XOP;HaQi<p1&7(ftu@HmBMG^bZcK,P;?X<`n%SX2u#&njM$F#S4SYu36k"ht1<&+:/>"0M3El.Mi0Hg?3!')KLMXY5)R*.DMS<=a5X=kW-OFnM8[;0<>.mg5pL+5+ILr%X[(#^:cO4nf<@7-nRO7Ee2MM*G.%/h-@LE#)fTpTeYVkigtQ%8SN8*i0+`miY;<*`IdWG)B-0,bAKn/C&&06&679)\ri@5(\GtNiWJUE.NhM`8cF9qeq6f"CaD5W!mkZe[mN)h9F:CXPYj0h`,3I9Kh.Ek@TWJU,9lBl9m1M7)L4'E?C!(kLe&u%F"MSHnT`UaNdc'ifFC_iOD+MpODqBhCq^^4]R)<<%O&5SCO'G6o+a;m&-oa2F)s>ANl#k$tB]W%#9K<`X_je>C!aA,'G[L?Ig;ji(iF+mC/^B.Oggc1hM53V:u:YZTj3Ae3QS^>+lb_:K<C3%S8RXU+%36Q7a+'8;9ScQ&%+%n8"=DcB9TL3a?-*&..kc'IeKg.JT_a;l!*W$ME;qQZT?.Y!u3KX&tp5EFcRaSC])HA$n.S9L97<XTGBTHA9U:T*_a-+T*-c?,HQY:jXN@8<GB$dUi@k(&)Dd;K@\aW[r-bRMVnA4g^EQG,0#,CL]50,+W%d_)>::A]%^2"gL<1:1VE2b6[>t,T,=S+'J!qj,i4H?"]*N_E;\U$l2#sPT2P1;BUaq6SqJSV_^Rt%3M5CegSNi=qTR`)QrQck'Fa-'cS)4$DFD6#i&XK@AK*\>/[RkJ,o3$/,oN[8"P/#$/u)%7e`X3X8@j_oa)"imD!'uO28dH=`?H;VX"Rco2m"G_QC*H))[r-U5^a>+@ZLbTr`pfp4h*Jd<q/\L'p7#fdAIH;u/0B:08+jD&=,-r>1#fmBAH-F.tHYbKS_F9AeKK!i(RW*srM]!^6oL#XFr5S'.(I0n#?KgkajU/+>W(YY"f3_iqi%W7A>]6T'7`J=n=[nFr8c:BdW?!=r6)*kO@1hm]+N9Q0ns#A2>7#7):gZML3gs,StXUkYl]W;iH:3CAo!;V(!TXt&Co&(>>[Y'r"lI4XjL<G!3eK8$uoJ5>8,(@TBG5*Z4PZ7?%P<"<;=Xpj5e3Ijnj!W9,@^`$Dp^o)R1$Jm7U5o@:`.#e/^KY(4]9=Enn>-:bG4/%9:+)/#'@SZHDXP2BiM`51`9,g5:=o*QtG)R9!0\4'E$>M'%NIppLV9@/U*TXlA,a_/VaV4frgpdF3A'?q`4J3NeGW6RnS&puNcDbR6`eh/:0Y\Df<@O'/VDWQF'[tc2(l1tMN_FJ:-q[e#D2b3)9tJQ#5+&0%973tINPIJ(I@Y&=LeEK)Q4>`FoV:&Y%pfmN$5Lr14n3)BcmH[n6*f$XZ?[/>G/uQ%A&dlW<DR$-8iJQ!ZrM5r(aiLZWjIi,Du.tp_."oBiah+EeA2l66@,_&BkF&5LoCI?$S;;iZedM0H'hmFD*'%(E/Ie3Mc.;aRmtdUM%Hp!]UH7p"TBJfb^3g2X>*$Kcj"o3+"jqh'&(5LV:bbaEf9ZbN/tIql_2g5YpBKp?d)"t[!C#BY>"Z.<F*[MJ/Kt+*__J=9"/`?A+7&c>+DLk\gIWG0W!CTdOsh619T"^W?<CmBcl;_3lSTs8%L6j4OF6u5dm__UMjLPj%n-nnT?86@BJrS#6Wb<g4RRaK?6a5]+"(;I5UA;bc"g.[B.prSnbY#eWs)P=%hk[/KfFh#qN85>I8#4g`X8%TBE#P=E8j>dDkA(40?PhVtV>$a0N_Ad5Y7cQ?jTgP.43uSG+M2U.)-Z6iIehltcXH=1[^F<Xdi#HWE>V"Dj-LBV[uOiJa.V0K!ch[^]'MSlL_ef-s5Y4ki(XS))VBFp/3,KKQQ8[fS=kf+<l.a8.5V')u<Zh;LF&S<5]A:0IU%1s%O+g_N\-G^mg$5sPZJ'b1'26=ub^nma"!_ZZqMf/eL5kj;qkE0FmCp#a[)MT/&s0nI@AS3Nk/6)OO"Mf>Y=%q*oT!W&=@RTN"UF`@_C38kIh6%1P=?GtKpI8;-o!VNa1gMY"ZO2U(LPl?XB5te4g=kEPY!d56<pB%@H"KeX5BhF)ZPC9UJCEWu+;B6sn-A&X<ZaJ7mmO9o8HAk#IN886u4XKAgLU,O1:j.g*]%<mR['dMB\tQCB5V,@09XFQ&!2<]P#q&PP$I^k.rT<`VP$'#.6i)pJe*+O";,=aVl"76i-\fYJkBh,]%/p4B$\qCR'spe=#%,ID@"nG3;V2=MVeIQ^DX,Rg'D3O!_#b@b+_VPfY<"8FT-!ep<nJ2C'jiFe*[&f=\_#DAg-N"FcKW.ULJ+0\qh;-3SmT_^]Qs"I^t,jtKc;Z;3d&+=9<i`$BAr#4,l)3ZA(u!**064i^&:iZ2!r,Na>*D<%P+^<%@6M-msoloPk9nFnEei4:)ES:SF.<"oq6DC7*?\CQ\W!eH\VlCk:u:94N?TVqKF<ZY!;)&)J(/<#s<.o;YaC#MbJIO[4\1si7EG[%$2`]%qmSo4eC+5+C;Xj>hH<scl:s*qunY!^8U"gim"Rp]AO8:\gj@,qLPF/]]7YC+pP)Y'pkl2H]gNsUm$!&Q9j+GKl)fo`@rR4rPa(_1<m9%<)gGu0^p,KQ/F]Dem?35nZpuec:Wg`#aW^2mp42]/m!"k7FA/Lf.]r/`A3!ZP(m@#K2s3+`^'6Y$Cq&*.q`QD~>endstream
|
||||
endobj
|
||||
90 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3217
|
||||
>>
|
||||
stream
|
||||
Gb!#^>Ar9=&UsJX=JVl=T3;qQH'Joq_A=LGD43GM$M6W_YU3EeA)>G+A?/3!HY7U;8WaW3T;&`U.+I\1c?s[T"sY"7EUpU#+`ok5K9$jXJgPPl<CoWC05*1&Gs9't:0!L";j14WX+hlM6P[n;r:)CKo-!U;*&IJP7eB)D[RZB+5S4g!?oY9^M$K@I!V+Jr1;t!A)qSMIN2AaR)7eqoHA$_M&*IW?N0b,D1^p9pB5$i<9n#Ak"@(#Gi1jVTib)WMLa2QHA9'UuLbF?.Aj3f;qqQa+gdhn"Cu="7V`VRM3BiJMPRmqF!jI7Vi8W0iIXRK5kRhusVe7FV3<Yq+)"8UfrtRt=`?NT>@2-AW;=3Q&GJ=[4hE0BSTgYVX83,!,C=O>'U?O7B_.YJq\RJZfq=m1]Z0TWRY2%Co=^AYSS!J8&fYQL$Mm,DV4`C]5+T9O3=d46NIuW-re&\5lfBKEDi`="""nQEiNrNUE8<!("$=5\G2F-0gJNN/3-^oK&:!b)E6[ga,C]_uANP!BSTC3)fE/%;PTod9P_bIoUJu<1)pc1bikY#=2fEqH`'LWF5?nMWD^@0>":R)=j)^>/:DbsDiA/h,`=V,U(8Wt9_`TNR5j-kRl4D=l+fJ%>38@mIN)W\D>gO%!LQ9Jha[.J4^D'>L5X:?@g%]F;9JR_oKa!+^e?rfCo,0C^\&*hXWYIBqe!#.\?[0V"^-b<1Lf'2&Y4ciL">8Ffdg4Kek4d.?T2?QC4!m0=)W,H,"GC5K?n8V=gP<Nq_C!<?fK.V/UFCPpRptE1Wa4PB/&@.,36%kV$+'cet?Tr^c]lY.g;\kte&Ub?+G*5XeL.](OBVq3$5M)n]/;,Ar'qHVrA_ApI>qP(?Yi2CtMa6#FNg;fac^&0.FdHIi>cj.$<m._jeGtm`8j-h+!'n`dE&56oRJ,ApC.ClTToR&0;GFIU8<Q]ABF9oeTe792Opm<Y<@iCeiREs>,X[DrU8'nQTHHPl?\"mGc*eJ,VCb\4:.=j%m#\B:%KZ,0FB.u>\dC&1Jqk9#V!KG-Md`nkF">>M3u+$\.3gN?$gPO:=P.Ks6m7^Glc5).LWldG/KYc%dOpAo*dZWEUCiFX$^+HF-905%LJ`2e]/1Z(\HOU(Mlm'!_%ZGGdp=WmX_;5sBJf*b@V20%6*E414%C:M"cuiBaD)rU5.B.G*%OS/%L%DFO+%lV<4t/N[%RKY&j2GjLbgO]GD2*6`M*uO3WA%c\>I+uQ`WZ%W*t3!CG''+a?f=fW%UL(AFeh%/`?N#,^<W"1$+m*XoTG03><C$GsSI>rU=p'Nm#qGZgDhR+>&2o;S+%GmM]gC-oPE.M)^pF5l6JL#QI1Hj"]F7+d!;#n1"kD094:B2-`Q(rGn&J=-.W/_/Dun>*8Y@nf0k85bBDL8j+jCPbXL27D]13i;oam,mPqL1sB8H*gb(tG9CYFP-SRE7&\kcrUn]8>(-03$j6TH!N/GiL$1d:Ur7qZ:*H[N$T9R$\kP6\/,]3_,+6AG22pgmpRb2'_nG+K`nSFJG]M1k=cghabE]9mc+_VdAhoI>8dk-Q00fjo/\k<!!/[V\!)NCljHJ!n4J`F=$r^XI///U4$fB_(!nEM]X:LTIH:?A8;RU"PjZmRG_aK^j#b!"(8]6K[D:>hLr<8)j&/Y(!?+6S&`)>EK8m.ui*(K:?l%hT21_Eae93js!.&pXIb.(4&R=e^`i@fh[s5XD<Icb'^LH8=5bnBFLb"j;HE0BXfE):3R'/-Dk4jB(V3VGJC,K+WSq)>WmBj4QZIAonG'W7>CMX+U,J"?bCd,ZJ6UkfWL2*=o6OP?(`O":Ph5Tm<soT/l&^Xdsl@s:Rq5+LaY"?qPmlWm!`%DUp2ACEZ`CT"eCQl+'t4p%^=jh"@C1?N!cMudBb"SQ=/p/[!(bWd'FoBu5Kk]XR4["D&+i;#9WET0Ce]_^su(#ben<nKAq*%6sM8oToEFd*aiOeQ^s0+0.>0"BOVl$>sE;C6mZ982OpR0AjT(Kk>JGh6"1=gsCf;VkGK;7fQKhLioW@YS/YVGtBtmM+sD9(:?&'qEba=>ZfaNpFXL%_tC%GeB/P/\i[io_tA0Ck6g6Cmdk>+uM":C:iC)4C[H,5t%JjK0Np7$t3&%d:K.X-V%'hMPMY7hpB#%a2Nc8-PX.FmRQ;/q0gEr7T<YKS+#%6XD>u373/mm)'o`__$YBoL6+<*b`!DqQ[5#o'?aip1sTDbV6=Z(P7CZ'>[5tXKj)[c1bk]KA0#2Xoe0-"?bSh'RuP/=I(LETr]Eman+(>dm;Jc'5>P2g]mLcKrq(>Ts791mM)P_%5:aB*E@q=r&>8;8PYZH.d>R\2UE*:d5MD6hVDNWh0WM[MY+cpkg*("iaCt$U`up/GK>J\B<dV]P0$/i14G*'N<rKHcYFoP;RJ.!@[u=ORPDgE?SfMO_%_&J">Otm8-W$AT<e^Zk[\L[b1"(7=q.]&od09"*1LF*\2KAmJ-/f5fn/d"3mMu^<bb-qio6<%uK((Gen"-NF*qM;J[V+`'5hCk`iQrT?[[>n<)SE#(_Q[\<<kseqmQPr=G*HN!U$3gj"e'ET6pB08<Pi?9k;rUkY#uM-1*!G^Q5<(Rn`iNN,(q%[JEGe;k;_kYZM-OS]<pF0"?.W52JfrU!B@4782$SX9[A/>?8'h7R^st-BtF1\$?BZkiWDuIL/Z(*k2B9rN)<_32M<1WQ"NqUAd5)3Al)[95Uph3Oq5@4VPL(3DCQ\7()5tW&tOpAiZVNDSQui%8n9n,XedS2j^Q+><?8&>Fa)`_*QgE:[,TB6AY%]Ohk\2Uotmjc]plfma1+?:LJSB-0QaY1E.]6[PBE(kDl'r8+!=.(\3M/,C)O+=M>,,Tcf2cu7I,Lq70=,OrZKq&)>,SkR@f5.%`+LYm4B4TE*alZjpsaXV%7CF.Z-q7qOkNnSsmg:/A8VVFWn"OaPKAXe$&UDCgGj(i<Ap(qSn)4Hco>]PdCp;1CK/8_8nMSr%_6M'Z+X[R?n*0L</%#o,j[2(%29u%s*nXV9_fM'(Ir%r'uc0qS\Qs8E"f%6nRrGeEtVbR:_<)K+1pKUL[u#:U.K)?&STk)R#mB(iRCW7+!28IW]]f)NSO?UT^:[[oH`&Z:dcPG$<[caBE_=%0`\4VO#1\LY1rTbl1GF*Q_,42KVpugf12.nqWC"Q)>qM22+SU1,KS&]S<\ZJC)cm!,SKP%6F:q7Mbs$~>endstream
|
||||
endobj
|
||||
91 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3439
|
||||
>>
|
||||
stream
|
||||
Gb"/)qfCN%&cUU9`K!%)3*1kZpnhsb*QM2#j_VnuPYPkk<^Ve,`.T<L55aWhol';N02W90P4k`eoHk(T!jE%`G!CAd,QSMWOQ?H;)W9ESHLD.O#pp$&MP.cG)Z#YmmmeWBWMkD\W1j@)8H_mK'qBL?kM(M>r*XA1&[cPZ^cE*liPG'j-'CS$&QG.!;2#&iqmlc[.6..j2,(a$j<phiF/:Y0&9OK,9NcXkB&FS`$CX_IrpN@/BLoEYK-6YPW#HlCWamT4/]VAME1EYkJj^jnh@LP+$B(NQ-Ms!f6)><+KLDI7r+@q2,`RCq3Sie3C85$6HA5\XCm9i@h<!Y+6ZNhnP<_p69\OPB)6L*=-X#h+;XQr0">b%k\Soq;0.OR?;)LYh7C/guW@[h^A9(m)=;FV^.-OIQ&:?hscgd<km.,N>MY0FojfUKSRgmk>-n,Oh`-S\BH+JnSa7D;#6^XHufsa'#IY'ZHq@D',obtXX]:.jZihAXYcS#+463"7D7'.0ilpVNJAeT;'qi4h\%D_Q[Lk7TSk_1(4g:dNGg'8%^l!4a?1Dl/X_lMJ&rqWKWC:6qh%@i,CZ\>uK*W"tRbX_u_Tdj?m0c9oWr-E<Bq_Ga#[RD_:S1RnI<-PT+QHQFEF:pe<gBfGJ,7.T5HpuZ[d"t:g\f,6;\XrKoIQJCS$)^h`3<P\o#3mi7_EpoJi&]^RTT:$>3#G)IdFB`,_:>bSRZ709^U1=@.i$lk-5X9I/J1m^3`c\:;<5GcIBq4D!Y8&5o&C#D/_<9AEgGRu0LM#Sd9`U>8UknH\/@uo(orr"?"6GXgo.j;c`nQpkWj2]mDC!p*Hrk/dI=dPkd1r8ZY6`Uk&cnMVSPE%:c&IU.bFHOs1TN\L86=L!WI.^FbD3\6c%F,>>!#+/rYuHZ.pMjr%+-<QA3lp@Z8(WL!@7<Bmlrc>BpI:q,.I+ndZr3V%.>'*D"XP*SALrOG]AcYa'E?X42]MMZq9cl]nddU<9GKYalPhm7q4i995;0(QH*-1O&U_#,Z+3TOPXUB40HppamT'JW#YfIDe\H7058>rjE*-@A]GK^4:qWh[AbRUdCnmBSFU;.B$_,jIj9MqG%$N=Z18I#05`nC&-pOE7qFq.,'>e]q.q7e?`j3GVn]ZVa'iZhcog,%ItkNO#+tKYP]Mj[DDO;a]jG@QkE/)=^WX/`5bBa&!fA:gduS?<iEUVG%[V0L.tUJ41\7!fp617:47N_b)lW\k=-\>Z_=\)8B8^<NVU"t."W^[;I.#='!pe?bd[W0ZI`.4CaTsFmngLmWfUa_cp[7]JhLgt&"HATpti@KbjPB>_k_Q6h$Mf5^f`^ijT6'5bR6QLmB6&!jD8n\o0$`H>mpcEVMEuQ>MQlXmS%JV\.HKM%,s:3W?KqDf%ZbGoF]0[?Y&C8=s#OqDf2hMe+(6HQQc-qqfLK_?+5I=e\_"hCmOR,G!srJ_^[;Nd^ei"iQmU*gJ%DL_EQ:hSHWgXqO:!gkXAIJQ5LYf1<$b<*qMTB(m8F,Q=ii=)VGa<q1D9rKNU[EjUWh=&LAC3+(rS87/!%?WNgfIT+Zrin^!;]<K6F'5Tl#P9,b)gk,"E>l0C/)XM5"Z;&eas:F/S.+=?[<o*B5h=$e\X;%0FAoki?BMOI]sd7L%mV'Yl:52s;aEI.ATpH@a=)S5J02dt_uru*)W^6M["M'FFi#UN>)C/7f+%7/;EF?\(aNT._]q;ZMHero_b=kU0(C=GVr`e'YK:dM53]lF/WI\J#9]FG&F2AK]iS**"\Haoa\Z!p=+95*?RW[\jS7d;QTai+8*U4b4Q8lG(fXBi940#3:s\<(t=Q=m+/QIIt`kfUJ\r4)a_aB9UJaHHBa=PjKhhhZ`B&Y4@RmsA,WVE^7:-&>HVX]FelI1]'e-$\n!T$:We=^*J]AX_Wq7q;=M^%RT+i:V$nIR5YIffqj:Z'",:e#XfnV=r-55J?6a_L;h,$-KVfSaC9M!!QH<7sPbsR+trQZLGp)V]g[DDS,)Ah+_tqen3r'WPNmf_nD;/1O%[.qj/!r>OK0d2b#obTc]PIk]^L8+8Y/em2`rg&^=KXYX2aVQ.K9oUTcsbkVH>>X<R6i%AicQQ"<i%i,;_CQa0.EA\$'he2*["]Z8[1@-QHFO,pDWQF*4"2P6P:@$Ds9></SmXU6W%e=VE<5Dl*_;\)o$@jHppo'u5HL0>1B/FgYi4KKNh-Lq6<JW3Chp]:lg6V;B$3`d#SX@:&7\I>($i%JJ:QQdpUa\N0K%WVo'N>-^Q#><d&l,XJL`59_U_2pJ)Co=i*:Dc%:NKo#+0I%bZVQdAC<A0=#.oQ<]b@!(9/d(R^@NC,+a->e4L4Q[C`f+T@Z0!WqQEhnsCqW^/[JrLE\J<QZiE\e6<rN+L;.u:)o;+Og`$8fHIFqZLl4f>t"Abr"#d!.!3]Z>hTUR50<,+UY(F2WjdBAMLHG9-NK'm]2d&Y:ppg?Z&)7E\]-:jS1TCka4;@Md1d1$,r/G(K;M$OZpeh*2?aJW*TgU#u_?k&=t&Q&i:#kNU(609aTrc%i@fbA615"U@MToeG-r5=ZI]u?c&3!q?0'3LW.j=fkaVBDPU=.%1V);7%krpNbGZ'a&n!e6c6PoUt_[6PR<C^niO=-hs:A@YZSI(RA^=WFqM<p\UB]hVB/%33Op^l^(OcQ!lIo6<<E,`?11[M;ieZ5luC!(^^q_9q,g6<$O&!mHacF)r%_Xm"e%POT1R9`+#F2%JjC=?p:r@a]3gPBV1N,0J]4iBN2#G>8ii[BlodD451BH=k!:[n#=*cUJ]bo0Am?0ZZqo1='h2l3lKlSWI+b/&XlhO-b2WrJB'W&B#c[qq_B-Y__+sS?GuKWMb[V;Zp<B.,i0?,uNcY/G9W&?7_U:@-@Fn8pX$QE#8WWIDZ+3@=&Ph/mDo2Jg$,dOA_$:r3#7qB[J.V1p+!0B>]bn4Upo/EIIjk]\,++/`)-rdXuK1.3$^\W44oJ?F*;[nSJ&X0c#*GhC@"+a@:Qa`nXp>i[DM)Ohh0G)'a1QD7BdIeWM]pW^p6Es4kMD*"(aX8^?e?9*RZJV(91YZ^]<:X,@]_P+Gd,A/#+kJ]6\tKB*fcfn`?NbI?LlgLI99aTBj\4cLc/YOV]70-O8E9.R=9$@6-g=HBBe>.(s()a2f]=.2RRR:UK*mT4*u6HU@M7enN:jBTDl%A'*oP7/<JCgg=28iBQ>5bY`CCC5`%5>)1F)+F'D<A&'(.&p9K[W^P5Id+JRD)1FJ@adaY^VA5J3]iKP:R_H)VKI2TB1MA$UR+35p8,%>'`t;IhU(MPSK4Em#$6H9"MP;.pW>,Ir6+0MCbWL=ol']EaU`WckOt5YPoONPr3Wn/r(p['i5Si_-p#$O;i%K')^amrb6W^&&>USeIr:f'!@:[k12eVp;O&OoQV@Q-<DWuV&f`pZ!SQ/o.p$]^?\W-#*W~>endstream
|
||||
endobj
|
||||
92 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2912
|
||||
>>
|
||||
stream
|
||||
Gb"/)CN%re(B'h3EBh#jN@a@s%!I1YEDt11:?jf#]79oLZ"R>T=]4l_lWNS(h^_imjbIBp:5WKmUhqSenrPu.aE*D55f=b-rB$SQHl>9I1BA]9?ii_.'Gt(BB5mEh6Lp;.+c%mN5uTZ=O/ZNp/=QQkdh=AdUr]4-VB?IBK2Ws]87D)4M-t(mDCDab];R$6]5"Gm'E&JQ;;$kDOop5W@kY!^*e::`HZ<Wg!*h(eQf4Kr:<XbC(<P`pSkT`0CN@Ku?Wta2C)'Xl19<6$"D*EtV?!_gC5"Ja3VE[pr!A!_;TQ':Z[sLf>gl.5&G0jY!"Zd?mT$H3=keHXRK??*Eg7)Mj(<8li/8(1lgm=`_ejm(X6gBC<QjiplM[GrhT97!Z2[5*88'+H\KrTkkTaa*Z#\$K?KDrK&(fDA";e3<0S4?QT'2oE\#dsYB*(Xm)d*t7DV!WSb!7pNn3>7r(LPJb_\UXS*uN(2C13+8#L@ZjkXTSSRso;:`KYXpmt]3<nb*X6O$)Y(CTsE@-JUI:H@@i_>j!bT_m8m"$P32jBb#lB(b;AL(!ZGN_[jMP0:Ka5?^T3l#eL%VIZ!S;plh3j(q+6q-NR>0h:*]`3q#W!fH!D&m!Eo_T.#/5^5\l&Y?f/4c"0T(>^HAR!QF<b;(lhF8QC!?6GEb#8>D;I%pRK#O+)@<kR>$]*QIcDWIa_d%WUI^N-\1i."ldEj,dG![@-7;7ka(7?m^6Rejgr&]^7"rbrcnnFS=,'qY"SV9AjX4)NSSH0b/s\Soa<T26\u_YH93Ba_^#72f>(lga[J=b\6>j%<'/L61_sCl<a!4Ik31Q0loo6<oV+Am_JfBs8I9f(m;&#"?(c(Y>,38^1GuS#]j,1QS.(RSMF2:"6IKO0!5Gtnoe34VFI#0<^Uo((66$G_TO]f3G(.%QUM0Yb=Vo@M?'N7PlP,*!DFC^!PBl`Ql&j*GZ=l#*NTD5WX.N_-%LJG_d"9p*[%JqS3n_3ji**!=k&`n+H;^(7JuJkHr3j/1K<F%6R5qb>DpG"J1Qrh2Ki*e)pTfTi[i.a"Cljm?#%reA3J\3s2.icq%5)[.i0QZ>V"WO0\bE^^=l8SH_7DZE8-/jh7l>Gp5`Wt)f@&q3]Q`HV/tG/XP!&+G]>:=K"uNKbNn\WNh>7]igj`d<Z!os0pC^TU,^*1IC_sn6Fa9ea1D0O0/SX9]#3*1J+u=.I90M"MFJVo7`m0#CFkm=^eAZG\]+BlR'G4#?hd$HY#\^B6pm,;)$Ds@-:#pXa]'bX_n-0GZ[+lJOj!ojRQM]>-UCqZ;LCEk1Sfa15S"V2jmXSKJU;i>2s5[up)]uf5Po)6EVNto>b(D@_<n2U]BFHV3^&4fROgnUZ.;1KQn[[<r9-7'0Y^1UaaodO,@HfUdO+q5T>nLo,B_n\+';Z<P4:FO_(G(]<pYUg4GJJ>JM;r?#++T\4(-G&r>XW&jFr:&-N#`%rg5]#FWJ[B-E^A;(*6obN%Jr^ds9l,<k*A^%tK][ILDf>('mTQg?a1-#FPQ_d=-H$#TnSX+t!6Dq:'?@;//V*[(4$D(>l4iLOP&uOD<.)lcbnZV//![POL0:#0m@9"b69B'#e-JBXod;]*]^0_;4E)nLPqsLTC'5()]%j];J'<C+S]m5stWX@g;"s6<UP4<HpG(bR+Y67R/#u2$+It(0"-hCQ/<_1!9V'A?0agkJ+^@Wf6s\[CmqhI8BdLEZ-8>FAUi7O3Ep`n`([Wp$lfLQDBQ`or+BVPIL9'IYEGn,\+^3O3NmXnS>=#_O%=+]a5dq*+DV:pjm](^=W1d_^W+k)rMCbY9K0TK\sU$M>:E$+^1jGGVgnHZJ9>koJ>g"Z>[A:9-1690=%G]P,q6_l:"aLZG6M+7G1Wn<aoRmjneWS6(qR\W`30YkHhq8iBL!M0c>iRM'@CZ2GqDRH^lMD2_A0#&O5R#=)`%_A8Gg7LNo9[KkZ>'#;.ra=lQ\CfhXRr1I$ht[rK^Xj5uf=G!;XoS5$]9<gfX,Wip0'Eh#<W;J*7PjKBf2fhS42j/sCfAN^+g.L2N7i(_1pUAd,(Ct%$W(ps5B,+.U7m(5d?@+$oZKOqt<O7<1nm43rjj7_U)>.lK/&dg7$lP"97J1(u>_%%`f;&7eH(IZ]>,>!T\:9U_Q&J53Ag4uqODeYD@%2lIEm:lnQMj-m/qBJNuU?cdc4JlK@as0GTIM@TaG3D#2q`-c540s'+W--Q0i+hP4&0rD^^&)gToJpnt//OSenf3eXN2[Uj^`256,q+Lf)[497:Rr[H_8L=B4OT(I)Kefm.:kIZ7!,t+n?,i776oPjJr/U!M_?TCo+sOl8_&R$Uo+F=?17&'F=o2]78`*^%81n%l.F$qiHqLii\QrIXp^XG8It3dVl:YSMKEe#N*.,*d94(%#nuk+GJOffmJj!ZkC5E3'[pJ@U.WJSH!u\FaJ0iQ8&Qr;H`GI;MWl,#BaC<o!h]GO!n(ESR^/M*#(n=l:2,sD@dC%GX+4p8-aSKiAHCY3NsK\D;"+=bbKphu'(iK#RU)YJZMU%'^nWc9_RHV;K^NQW6>>*GYle%b#RbG9i\L!ie)u4h^H\T[%@\=l#</O4h<d?P6.hOt$Y`[REaq<BP0riXrOD.Zm`*sqAGbC44gIWGhdC>U:V-h;=XsdBKSHu^25mHo>WR.II]MLT%eJ:R#eYHm>WEi#-7&%b6->t]'pc[nFom=S&C(D[rKr?(aq4D@S@QssQ%#Ni!+34sIY"Q9:f?pL(;MrVnk`;jk+RS>1u-RLK`V`5!\8/73,+NB)0c,*_NcX)LFt4-<pYe(1%B)8bA^,@;lkV`@l>lC'2NVoo=_Nd1gS@ScWr6+Fc%e"R\H,=?-j<kh)*d^]s9/ocC'9Mq\9i4eF]]FC`9h4#q@)f7S_-W2DVYj'0,e0+ruV%~>endstream
|
||||
endobj
|
||||
93 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2896
|
||||
>>
|
||||
stream
|
||||
Gb!#^gN)%.&q0LUoV43PP6?-\b`e73'@4[T7oJKk<@N`W,UO5)+\5u6laK0B'IiW'HagF_Nm+0L(SK1=1ggA'IRs19]*3Go!;2c\Ts.W"0ba^*T4,;jqr`q!P(or<Ni<n(gU<j@'*I.0@=43%\d3l+(+>G$X4K1T?=flM`+q=[<.U*X(IZSR*[^>`nH0QA82GbSk?uGQci>f:U6CH\7<>DFlCd>S9=#X8Cm.U&Jr7l@q3k`bNt[3[C;D4D+m-$*"a.s`+et5J)'r3p_!uX=(3(gB@<;sI-e8T]*PL'7>AH"MUXsI2p$1ZGrhB@oH,t#^Jt8_LI??cU:;=MMas&(qfgB?APStlH_>-gB\#jpBFfJQ4Fctb!=ci2!c.0;bDWln0C'+m-WkitlgdYTgE7hF+1E`P\-B+s]-$+S,cr8CQ\.kD\:3ak?<_I*mZc>Nij4btBT!npm-VeH&R-^k@meNP9XSq3=<F'<7:J)>)<>Kn#jEiKmanK'\rSVSA&6S2R3S^53r^5r(\L?V;ra!PY39Jj2\=V^3]RL+mbqBg?9n:A"<t0BuGj*/n]0qqSW'Vr2UcDn?6F`V17VU>V;8/ZS(V+N"[9)TJdQuT&M5ibD=FGRE<.i8'_'TeO\k18?:t]_6<2"KR@SXq_'!D3&WBUftF2.*`pIMWW@@<Q%:fdMbYZ?VerS<J](M,e]A=*5"QHRE#B'W2ce3P1i=mnQ1?!cFEp4kh<opfm(O@<"H9L+hYGRTrpEqLQ4_;h-UZ@mRO&UssM`&r9TEQl9fF9G0pZ(BaVfOl;`pBO9Z(e]!=P2d!3G;jom+Q%?f#+sqmI32Eoak0Fe+^5-QMJMC(CJ<6]4!(^2=2p5C4r7$2`s[^P:r(Xhj1$1b(Z:'dOEr==AL\CNGt@"`bJe;enk]2ZEMb_F2G=1mhm5Pm'GDN\Y-pK4$2GFR4<)_K"'Y*,[>J?\Ud^F,HIX0d/;K>6<JU(K(d]9F)8W4[!4c9E!2#,L]B4ZmE7q0-0b-+'HW::>W>Ilm<>iTMr;]8)S?9E1<'D#$rcOGO,u;"KV:[]J[W\tWYnWW7*,c3i.a0*2amVBZGNsP899^S!q^;&HmZk<Clu?`nM8k/'L<5nR^]NE:EXRO>qn(p65NMtEqiZFF4FZd*/ekQN<gnc"+$_FY^&Xbl(/JZ#09neF`R*?e_K8='5.704'ps&$NW%qK\",7XE@T6*FYfK;bMaK=":fAEG=`Q1U$'cM]mAol4VI#IOa;?aB*_eAFTr4)OmW3u2d5`&I8SdTG9"%K)PXb],FJZ]2l[_3j2RQR8<Ng+G_QY@)[).6"\[-M*im.bGN5GUUh?`r)oTA/X.A?`9G6Sq\dWs&q/dDT*T-b=B@m*R)l0ZMHGNG7,D#h#3?*O&`G<$D8T\3I$+1LBIT5L/M7@dRWjPD6&QE9mgmP%%M%cpco9La4$U$Rh@jM*CjFkh2q*qq/@a]V5WpYDU5J_kDGp0L9ZR5C..1(YQOlGEl)3QroP5O`l6<@i2FSkko>,/LuJFeE@Wk\$.V"$aXL/f,pomQbM$n@L3?=juhM@Jk[CimSj@F,c-XnI[!Q-k^>qT6Xu,nmd>=*HU<"7rsM#iBMB'p9)K+XBGN'r2pul.7XpX\MPID'6_J"^;g]71nO/Q'0i+8m5`Vp43l]2+h[*(bqb/Q'+lPPdJGcWE#YDF`uTc<HG?J\Z0)-$5O;gQ(f.^Tieijlh^'ACWRG%p57rsdSCXoVq\6_)/DZ'"I[oPPHsiUA%I2;8o=>__f*GaVs?4_i*0Xj^GIL934`P+H]Jh;#`N>^L&t_KP4l2X+!Af2(4$Pe>JHqF(5u2$[#3Z\b%W^S$.g#a8P``5s0'Yn*-0P*,B0::X:I9])QicZ?a2FI<]urm)P`?3DV5\NRsg$AL[-+]dGlfTc#DMRe>:>48CrKB*cE7c0Q2Zq1C9DaYbW/?*K-pu:d9(9V(q<J^A"T)QH4H=HG42=d#9N3rrnLUB]-..!VbbpS8iMDZ2%foj0A;s(q+0L/XAC*KM/jf%'HN`CF;"erEfS?mQrFqlDC>DLmP6"U`0RO\\Ml@^#_iP]5kSQM3I]&IOc9p\5=.k(LT?gWTMYC`,Zr1n@CueJ,)@f/rm.Tf^L^Z0&E3W,4QWS[Q=DBWRafZFJ>*'A&6_q@)9IdOTJZ7cJQH/+0mf]3nFqiBY7gB]8pf^c(c6H=+]Lp?FLE-m(&nj%(pFk<#iHMT`>Nlace>R+:X*jD`[m3ZgkJ)9k=+6]]kWL-]I%:$iGMGM[ThD@"F3E/D2#gk#pC=N2M3MI#G,R)kg@5?+%6sqEbqFCiRWr*!X.iT%2,^#G]uA>GJLY#lKbd2OPf9]CR=uh!bmKQ\PE\[k4^f?T/7RD@e)l(EIFIFefuB./l-03k?tZkR+#IFtf_FPQG>,_nu*u:#rj_E2+Gjh"_':\;N"WP)6#;V9Pn(`>[<b_"YdUiNjX*"GGPuG-PkK+T$4ihOW4&qi4[5q'>hZ;NXB%X;2+]2j(n/>s?F6Q1k'(Y`Bh)/*"C]:%k)J,4b,5J3hA+D88)=_Xi+Pgt0F;3;Sa8PmUdPr<nr56\eHq`>u%AhoNto,WC!?K9[O:Aifh+;6&!"fhQLBY*B&ue(%K,`e3Vm?N.XW!,mr4fqks;VCal.62^;`4E<^po&0,[!L>UL'';cORmCXE^%ut8<OUc@/`F"c.LJ<52(uH;Y"]<b-XH[^IkH/[Sm@UF>Yb`l0TUpeGrSp0&'4t/`R3-7LE/`Wjf9Au)T?_j[.!VNm-gDSe+mDRo/g*NB2"m<hk0B2CCG4T3$dO$rRK@Wqhf08F]\F(2&c?RkTn/!(uU&ofsOgt42j)`Yq7s74tt7*%0WZ#C0'+IOZ_:m_uXU&-6Z.Hq=t.moXa/kSG`\-^@(^~>endstream
|
||||
endobj
|
||||
94 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2141
|
||||
>>
|
||||
stream
|
||||
Gau0DD0+E#&H;*)YsZtp:;]YI8-73/A8m-1B:OVTRH[CZ4[L,2>=aWH34MQZHh[.666HB%(5tQ5]m[M@G5"Bj`A`eVgN.n'!@X9K."f>=W(&T\gd#Lko(@[5S=333VQO[\BJbNK2`4iqA6_Z,Z7(FC*#OltPtMm":L^=2i=r<!8YG6",0-1iYj2&CgkqU,5!1oZ_&Wqj8r]J5]h>EBLq[(D&u8^2b3thPO63=r4t:Y74Nr0b6**_i7$3Y3.`b"`/r:ZF;aS(,8hs/-hIq,:_=GSWo!&jK3s%.C64VD%K+qQmr]:6`1_V!7`f@S#*A;.Ji$Vb+q.hqMP;l)F+*Xj63ltX\$p_Lt6pNeEaIa,[_[TRX;&"hPU(huC"0)?Y'fa><<G($,UD^.>W?:u6W$0WL<TH%dn@nqe,:$01L_hj3N4AV0nX4o9oj)21?PbhP"@%S-Vu@f>^!<4>eute<?+>gW:D.'<4[\Pu*csW\b=bUk61K*)jLWLb.5\,\I*.d-psZ6;e=-3bGN0DqIMNL"1J^ue)RL[ZA1*#W?n6t!'1nrFM=ef1\/0#XN\>>9GY</=Z@e,7iRr,67;%8$^$=`(S&WS&<9][$0AcGdj9e5'q03i\69qTNDOM1kH$T=23Sp,V8MCPG>Pt,,p)mdu<QO:\1ILr-TM+@B!;O=G;!'S!KGYq0X".>e!a6aZA[!p..T*I_Q,?3QM4QIBh1mUE/XS(o(V]OEj(*<-(a:%S3lS7Tl!*@!nBJd!?!t:]^=.\^(5BmD*8:u7>lPChk6(gHL1b%H)_s6k'o/R?]KeC`L.:.^mDO"T-sF-t@sjgRbla+E84CqB@cTSYZAI&#"tY/@-#kLbo#]H(B3X0#%uQK8W+"9#Es2fJN8`D\aX#6Qh=AS2'&#$M$k)kTFM[\Lq[n;NS#;`&0im"P(OVP!qdPY6$dZ.K3&:k&F99[kO_*FF2%&&69C!-"Q%UWO0q\?PdQSjeq^p9cNKn(1[O/$>-,#-g"#L68gAT=m'*D%MenOk>^%4aD&_<[q+])r+o$Oihj.P5GWcro\M&c*nqThOgnORr@+[g<uDlU=-GQ1jXInicq&[@^J[qZT(LWU!=gZH"G.H.E-OYjSm2/0iV0HB5tDs[m,htS6*U>"N[i%;0b!24p![I5Pg_Y6e5$c[/^]n3gFXibA>7H,KiH;ti&*)jc9FqUMDFqc*8h,)XoS%eR!54o+AJur-s`9d>&j8-^0T)/mh6@TJUJR&s_Ym>_Qn<H:rf:mbShSp0Yn,oO2&"qL)e%^[gX21J+j2Ok7C(8M3!aP=_M<4gKIIX\R)*=>ko2\GCf>@?=T4<SSltZi0;^\#+NDbRJ%?6`(_pKTRS(?[]k$JKO/B*sbl'<EG-QMeY"B;psR^urfZ4Pu^$177&p[?;u!L?Qq<)HpNWhCT)@:C4jjZ3YtG*'2gFp)\D2R'YRba\n>Cj=M\Q>AeDTbV!aNtDeAbHn'n/gjX8O-N%DSNNGXc>g20BWLje3N^BRQTG0[hciguZQ?oj_QiI2eiZ+tidVZj_a4pn0AGVKK.:iFE*TI^_m<G&q%6_3"BF\Np>u>_*qG59a")'f7MF,&</8%^cNTZ69ifm35jG5?2OZlL3\rWCZd6O#3l>5X$)>^p)TEgoTp(fk;SH'Fm-Zr;81[Mgichs>H2#G"]2,]MNuD-KM(.2*c@Ci;PA!RmPR9*S+LQ&h+\DjXlAQR[nLVe?W!L3:@[RnkNj.XHl=aM[#7#<OOU7'T%7sPpP$@;I/lT6]`Cs[*N^%<H=n\s<>%2Y*0O7aOT$0ap)tYc:Di1<R-#5f/<@Ms"^aq@S%[bU5#3Wu<.38N/VpX0>QK&]O9KurG4'#[3&Pb@E>_,D-nH&N3J:&q%J%[5!Eo5p?mY!E"6dmIu^O,EEYl9*pWl9>p>.emL*u!0%I<FSY]&q1UlkY5M-:bkPN.=ti!rca6X;a/$?6o`obTo_We*8.-oXV%Ti<VdhVTDFLA6i7C9,WaRmX28*RH&:kj+el]RmI,bE`+?nOB;"sGNY"ic=[NieY4s`&&.3jDOp.TM88^K,$-ZpGqO%qZL0j@XCoW66dDg@+UsFZL&o9^Fhru;@cZ!qQ]%0TRM\NJ"GXfTV9Lu?<N9MZT^%7Y3;FB\.Z_U~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 95
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000146 00000 n
|
||||
0000000253 00000 n
|
||||
0000000365 00000 n
|
||||
0000000513 00000 n
|
||||
0000000661 00000 n
|
||||
0000000808 00000 n
|
||||
0000000956 00000 n
|
||||
0000001104 00000 n
|
||||
0000001252 00000 n
|
||||
0000001400 00000 n
|
||||
0000001549 00000 n
|
||||
0000001696 00000 n
|
||||
0000001845 00000 n
|
||||
0000001994 00000 n
|
||||
0000002141 00000 n
|
||||
0000002290 00000 n
|
||||
0000002438 00000 n
|
||||
0000002586 00000 n
|
||||
0000002734 00000 n
|
||||
0000002883 00000 n
|
||||
0000003031 00000 n
|
||||
0000003180 00000 n
|
||||
0000003327 00000 n
|
||||
0000003476 00000 n
|
||||
0000003625 00000 n
|
||||
0000003773 00000 n
|
||||
0000003922 00000 n
|
||||
0000004070 00000 n
|
||||
0000004219 00000 n
|
||||
0000004367 00000 n
|
||||
0000004515 00000 n
|
||||
0000004663 00000 n
|
||||
0000004811 00000 n
|
||||
0000004960 00000 n
|
||||
0000005076 00000 n
|
||||
0000005182 00000 n
|
||||
0000005260 00000 n
|
||||
0000005409 00000 n
|
||||
0000005493 00000 n
|
||||
0000005699 00000 n
|
||||
0000006137 00000 n
|
||||
0000006343 00000 n
|
||||
0000006549 00000 n
|
||||
0000006774 00000 n
|
||||
0000006980 00000 n
|
||||
0000007186 00000 n
|
||||
0000007392 00000 n
|
||||
0000007598 00000 n
|
||||
0000007804 00000 n
|
||||
0000007891 00000 n
|
||||
0000008206 00000 n
|
||||
0000008281 00000 n
|
||||
0000008373 00000 n
|
||||
0000008476 00000 n
|
||||
0000008625 00000 n
|
||||
0000008715 00000 n
|
||||
0000008807 00000 n
|
||||
0000008917 00000 n
|
||||
0000009039 00000 n
|
||||
0000009184 00000 n
|
||||
0000009286 00000 n
|
||||
0000009402 00000 n
|
||||
0000009515 00000 n
|
||||
0000009630 00000 n
|
||||
0000009718 00000 n
|
||||
0000009848 00000 n
|
||||
0000009960 00000 n
|
||||
0000010112 00000 n
|
||||
0000010203 00000 n
|
||||
0000010315 00000 n
|
||||
0000010425 00000 n
|
||||
0000010520 00000 n
|
||||
0000010627 00000 n
|
||||
0000010733 00000 n
|
||||
0000010842 00000 n
|
||||
0000010949 00000 n
|
||||
0000011072 00000 n
|
||||
0000011176 00000 n
|
||||
0000011294 00000 n
|
||||
0000011405 00000 n
|
||||
0000011525 00000 n
|
||||
0000011653 00000 n
|
||||
0000011747 00000 n
|
||||
0000011872 00000 n
|
||||
0000012819 00000 n
|
||||
0000013945 00000 n
|
||||
0000014176 00000 n
|
||||
0000014407 00000 n
|
||||
0000018061 00000 n
|
||||
0000021370 00000 n
|
||||
0000024901 00000 n
|
||||
0000027905 00000 n
|
||||
0000030893 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<dfb56bfa7fa29c8df90718f7ff48e788><dfb56bfa7fa29c8df90718f7ff48e788>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 51 0 R
|
||||
/Root 50 0 R
|
||||
/Size 95
|
||||
>>
|
||||
startxref
|
||||
33126
|
||||
%%EOF
|
||||
753
specs/20100612_1_scaling.md
Normal file
753
specs/20100612_1_scaling.md
Normal file
@@ -0,0 +1,753 @@
|
||||
# Platform scaling and multi-environment architecture — specification
|
||||
|
||||
<!-- doc-meta:start -->
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Author | Anton Afanasyeu |
|
||||
| Revision | R1 |
|
||||
| Creation date | 2026-06-12 |
|
||||
| Last modification date | 2026-06-12 |
|
||||
| Co-authored | |
|
||||
| Severity | medium |
|
||||
| State | scheduled |
|
||||
| Document type | spec |
|
||||
<!-- doc-meta:end -->
|
||||
|
||||
\newpage
|
||||
\newpage
|
||||
|
||||
---
|
||||
|
||||
|
||||
**Document type:** SPEC
|
||||
**Source draft:** [docs/drafts/20100612_1_scaling.txt](../drafts/20100612_1_scaling.txt)
|
||||
**Design review:** [docs/DRs/20100612_1_scaling.md](../DRs/20100612_1_scaling.md)
|
||||
**PDF:** [20100612_1_scaling.pdf](20100612_1_scaling.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh` (Mermaid → PNG diagrams; requires `npx`)
|
||||
**Date:** 2026-06-12
|
||||
**Status:** Planning / brainstorming (pre-implementation)
|
||||
**Severity:** Strategic (post-alpha evolution)
|
||||
**Scope:** FE/BE topology, service decomposition, dev/staging/prod clusters, cloud migration path
|
||||
|
||||
**Documentation index:** [README.md](../README.md)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
|
||||
**Documentation index:** [README.md](../README.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Table of contents
|
||||
|
||||
<!-- toc -->
|
||||
- [1. Purpose](#1-purpose)
|
||||
- [2. Vocabulary](#2-vocabulary)
|
||||
- [3. Current state (baseline)](#3-current-state-baseline)
|
||||
- [4. Goals and non-goals](#4-goals-and-non-goals)
|
||||
- [Goals](#goals)
|
||||
- [Non-goals (this revision)](#non-goals-this-revision)
|
||||
- [5. Requirements](#5-requirements)
|
||||
- [5.1 Must (platform)](#51-must-platform)
|
||||
- [5.2 Should](#52-should)
|
||||
- [5.3 Future capability slots (normative intent)](#53-future-capability-slots-normative-intent)
|
||||
- [6. Target architecture](#6-target-architecture)
|
||||
- [6.1 Environment tiers](#61-environment-tiers)
|
||||
- [6.2 Service domains](#62-service-domains)
|
||||
- [6.3 Reference cluster layout (prod)](#63-reference-cluster-layout-prod)
|
||||
- [6.4 Traffic and control planes](#64-traffic-and-control-planes)
|
||||
- [7. Service inventory and scaling profile](#7-service-inventory-and-scaling-profile)
|
||||
- [8. Deployment and promotion pipeline](#8-deployment-and-promotion-pipeline)
|
||||
- [9. Zero-downtime and load balancing](#9-zero-downtime-and-load-balancing)
|
||||
- [10. Data and storage](#10-data-and-storage)
|
||||
- [11. Simulation lab (Alpine HVMs)](#11-simulation-lab-alpine-hvms)
|
||||
- [12. Capacity planning framework](#12-capacity-planning-framework)
|
||||
- [12.1 RSSH / remote access connections](#121-rssh-remote-access-connections)
|
||||
- [12.2 Cast and media paths (future)](#122-cast-and-media-paths-future)
|
||||
- [12.3 Network bandwidth and egress](#123-network-bandwidth-and-egress)
|
||||
- [13. Cloud migration (GCP / Azure / other)](#13-cloud-migration-gcp-azure-other)
|
||||
- [14. Global availability and GEO policy](#14-global-availability-and-geo-policy)
|
||||
- [14.1 Reach model](#141-reach-model)
|
||||
- [14.2 Segment isolation](#142-segment-isolation)
|
||||
- [14.3 Enforcement layers](#143-enforcement-layers)
|
||||
- [15. RBAC and operator model evolution](#15-rbac-and-operator-model-evolution)
|
||||
- [16. Commercial and paid services (platform hooks)](#16-commercial-and-paid-services-platform-hooks)
|
||||
- [17. Observability and SLO](#17-observability-and-slo)
|
||||
- [18. Safety constraints (current topology)](#18-safety-constraints-current-topology)
|
||||
- [19. Phased delivery map](#19-phased-delivery-map)
|
||||
- [20. Out of scope (this SPEC revision)](#20-out-of-scope-this-spec-revision)
|
||||
- [21. Related docs](#21-related-docs)
|
||||
- [Changelog](#changelog)
|
||||
<!-- /toc -->
|
||||
|
||||
**Documentation index:** [README.md](README.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Define how Android Cast evolves from a **single Alpine BE HVM** hosting all backend services into a **multi-VM, multi-environment platform** that supports:
|
||||
|
||||
- Stable **dev → staging → prod** promotion
|
||||
- **Horizontal scaling** and **zero-downtime** deploys where services need it
|
||||
- Optional **cloud** footprint (GCP, Azure, or hybrid) without breaking the existing Gentoo FE → Alpine BE production path
|
||||
- **Network bandwidth** budgets and egress planning alongside CPU/RAM/connection limits
|
||||
- **Global availability** with **GEO segment isolation** (worldwide reach by default; denylisted regions per content/policy — e.g. no publish in Zambia)
|
||||
- Room for **future RT/AV**, **VPN**, **payments**, and **third-party streaming** integrations described in the draft
|
||||
|
||||
This SPEC is **normative for planning**; individual services retain their own SPEC/DR pairs (URL shortener, remote access, SFU, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 2. Vocabulary
|
||||
|
||||
| Term | Meaning |
|
||||
|------|---------|
|
||||
| **FE** | Forward-End — Gentoo bare-metal / XEN host; TLS, public nginx, UDP DNAT |
|
||||
| **BE** | Back-End — Alpine HVM(s); app services today on `artc0.intra.raptor.org` |
|
||||
| **Cluster** | Named set of VMs + shared config for one **environment tier** (dev, staging, prod) |
|
||||
| **Control plane** | HTTPS APIs, consoles, heartbeats, RBAC, orchestration metadata |
|
||||
| **Media plane** | UDP/TCP cast, SFU/MCU relay, ffmpeg transcode pipelines (future) |
|
||||
| **Sysop** | System operator (PO today) — owns infra simulation and promotion decisions |
|
||||
| **PROD** | Production cluster serving end users |
|
||||
| **RSSH** | Reverse-SSH remote access track ([REMOTE_ACCESS_IMPL](../REMOTE_ACCESS_IMPL.md)) |
|
||||
| **GEO segment** | Country/region group (ISO 3166-1 alpha-2) used in allow/deny policy — e.g. `ZM` (Zambia) |
|
||||
| **geo_policy** | Per-resource or catalog rule: where content/API may be **served**, **published**, or **streamed** |
|
||||
| **Egress budget** | Planned GB/month or Mbps peak per service/link; triggers scale or CDN move |
|
||||
|
||||
---
|
||||
|
||||
## 3. Current state (baseline)
|
||||
|
||||
Today one **BE Alpine VM** co-locates (see [20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md)):
|
||||
|
||||
| Domain | Services | Stack |
|
||||
|--------|----------|-------|
|
||||
| **Web / console** | Hub, crashes, tickets, graphs, builder UI | nginx + PHP-FPM |
|
||||
| **APIs** | Upload, heartbeat, remote access, graphs ingest, short links admin | PHP |
|
||||
| **Git** | Gitea mirrors (`AndroidCast/*`) | Gitea + MariaDB/SQLite |
|
||||
| **Database** | `androidcast_crashes`, `url_shortener`, Gitea DB | MariaDB |
|
||||
| **OTA** | Channel JSON + artifacts | nginx static / PHP |
|
||||
| **VPN** | WireGuard lab (`wg0`), RSSH bastion | kernel + sshd |
|
||||
| **URL shortener** | `s.f0xx.org` | nginx + PHP (submodule) |
|
||||
| **Build** | Docker builder orchestration | Docker on BE |
|
||||
|
||||
```text
|
||||
Internet → router (134.17.26.161) → FE (10.7.0.10) → BE (10.7.16.128) :80
|
||||
```
|
||||
|
||||
**Constraint:** This topology is **production truth** until a phased migration explicitly replaces it ([INFRA.md](../INFRA.md) §1, [bottomline_reminder.txt](../../bottomline_reminder.txt)).
|
||||
|
||||
---
|
||||
|
||||
## 4. Goals and non-goals
|
||||
|
||||
### Goals
|
||||
|
||||
| ID | Goal |
|
||||
|----|------|
|
||||
| G1 | Split services by **scaling profile** (CPU/RAM vs storage vs RT media) |
|
||||
| G2 | Three **environment tiers**: dev (unstable), staging (beta), prod (stable) |
|
||||
| G3 | **Seamless deploy/update** with rollback per service |
|
||||
| G4 | **Cost-aware** sizing — no oversized DB VMs; burst CPU for build/ffmpeg |
|
||||
| G5 | **Simulation lab** on Alpine HVMs before cloud spend |
|
||||
| G6 | Provider **L4/L7 load balancers** ready for stateless tiers |
|
||||
| G7 | Preserve **dual-remote** git flow and existing public URLs during transition |
|
||||
| G8 | **Bandwidth visibility** — per-link and per-service throughput metrics before saturation |
|
||||
| G9 | **Global reach** for public services; **segment isolation** when PO denies regions for specific content |
|
||||
|
||||
### Non-goals (this revision)
|
||||
|
||||
| ID | Non-goal |
|
||||
|----|----------|
|
||||
| NG1 | Alpha release blocker — scaling work is **post-alpha** unless PO reprioritizes |
|
||||
| NG2 | Immediate full cloud cutover |
|
||||
| NG3 | Replacing Gentoo FE in phase 1 |
|
||||
| NG4 | Implementing payments, SFU, or DNS resale in this SPEC — only **platform hooks** |
|
||||
| NG5 | Full **multi-region active-active** prod on day one — phased via CDN → hybrid → regional PoPs |
|
||||
| NG6 | Legal counsel for country block lists — platform provides **policy hooks**; PO owns compliance text |
|
||||
|
||||
---
|
||||
|
||||
## 5. Requirements
|
||||
|
||||
### 5.1 Must (platform)
|
||||
|
||||
| ID | Requirement |
|
||||
|----|-------------|
|
||||
| P1 | Every deployable service has an **owner**, **health endpoint**, and **config outside git** documented |
|
||||
| P2 | **Prod** changes require promotion from staging (except hotfix path per [GIT_FLOW.md](../GIT_FLOW.md)) |
|
||||
| P3 | **Stateless** HTTP tiers scale behind a load balancer; **stateful** tiers (DB, object store) have backup/restore runbooks |
|
||||
| P4 | **FE** remains TLS termination and edge routing; BE/cluster addresses are **internal** until cloud edge replaces FE |
|
||||
| P5 | **WireGuard UDP** and **RSSH TCP** paths remain explicit in firewall/DNAT docs when split across VMs |
|
||||
| P6 | Simulation cluster MUST be reproducible from repo scripts (Alpine + package sets) — see [§11](#11-simulation-lab-alpine-hvms) |
|
||||
| P7 | No single VM hosts **both** primary MariaDB and **unbounded** ffmpeg transcode workers |
|
||||
| P8 | Every **bandwidth-heavy** service (OTA, object store, SFU, ffmpeg, CDN origin) has documented **Mbps / GB-month budgets** and alert thresholds |
|
||||
| P9 | **geo_policy** model: default **global serve**; explicit **deny segments** (countries/regions) per content class or tenant — enforceable at edge and BE |
|
||||
|
||||
### 5.2 Should
|
||||
|
||||
| ID | Requirement |
|
||||
|----|-------------|
|
||||
| S1 | Use **managed** MariaDB / object storage in cloud phase when cost ≤ ops time |
|
||||
| S2 | **Blue/green** or rolling deploy for PHP-FPM pools and Gitea |
|
||||
| S3 | Central **secrets** store (SOPS, Vault, or cloud SM) — no secrets in git |
|
||||
| S4 | **Per-environment** DNS: `dev.apps…`, `staging.apps…`, prod unchanged |
|
||||
| S5 | Connection and CPU **dashboards** before scaling RSSH and SFU |
|
||||
| S6 | **IaC** fragments in repo (`deploy/`, nginx snippets) for each new VM role |
|
||||
| S7 | **CDN or edge cache** for OTA/APK and static consoles before scaling origin VM count for bandwidth |
|
||||
| S8 | **GeoDNS or latency routing** when a second region/PoP is added; single canonical URL preserved |
|
||||
|
||||
### 5.3 Future capability slots (normative intent)
|
||||
|
||||
Draft “future key roles” map to **service domains** (implementation deferred):
|
||||
|
||||
| Slot | Description | Depends on |
|
||||
|------|-------------|------------|
|
||||
| **F1 Multipoint cast** | MCU/SFU relay outside LAN; TURN/STUN | SFU SPEC, signaling |
|
||||
| **F2 Stream transcode** | ffmpeg any→any; RT and non-RT | Media worker pool, object store |
|
||||
| **F3 Cast storage** | Recordings on BE; paywalled retrieval | F2, billing |
|
||||
| **F4 Commercial VPN** | Anonymous RT VPN; capacity planning | WG pool, payments |
|
||||
| **F5 Payments** | Low fixed cost at scale; usage-based optional | F3, F4, add-ons |
|
||||
| **F6 Mobile add-ons** | Masks, background replace | On-device or cloud GPU |
|
||||
| **F7 In-app transfers** | P2P or ledger between users | F5, compliance |
|
||||
| **F8 DNS / subdomains** | User subdomains + site builder | FE DNS API, tenant isolation |
|
||||
| **F9 3rd-party streaming** | Ingest HLS/m3u8 or RTMP; re-emit as cast | F2, cast handlers |
|
||||
| **F10 GEO enforcement** | Region deny/allow on publish, stream, download | F3, F5, edge CDN/WAF |
|
||||
|
||||
---
|
||||
|
||||
## 6. Target architecture
|
||||
|
||||
### 6.1 Environment tiers
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Dev["DEV cluster — unstable features"]
|
||||
D_FE["Edge / FE stub or shared"]
|
||||
D_SVC["N × Alpine service VMs"]
|
||||
D_DB["MariaDB (disposable)"]
|
||||
end
|
||||
|
||||
subgraph Staging["STAGING cluster — beta"]
|
||||
ST_FE["FE mirror or path prefix"]
|
||||
ST_SVC["Prod-shaped VM count"]
|
||||
ST_DB["MariaDB + anonymized seed"]
|
||||
end
|
||||
|
||||
subgraph Prod["PROD cluster — current + expanded"]
|
||||
P_FE["Gentoo FE apps.f0xx.org"]
|
||||
P_SVC["BE VMs + future workers"]
|
||||
P_DB["MariaDB primary + replicas"]
|
||||
end
|
||||
|
||||
Dev -->|"promote on PO sign-off"| Staging
|
||||
Staging -->|"promote on stability gate"| Prod
|
||||
```
|
||||
|
||||
| Tier | Audience | Data | Deploy freedom |
|
||||
|------|----------|------|----------------|
|
||||
| **dev** | Developers, agents | Synthetic / disposable | Daily; may break |
|
||||
| **staging** | PO, QA, friendly beta | Anonymized prod subset | Weekly; feature flags |
|
||||
| **prod** | End users | Real | Hotfix + scheduled; zero-downtime target |
|
||||
|
||||
### 6.2 Service domains
|
||||
|
||||
Logical grouping for VM assignment (see DR §Architecture options):
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Edge["Edge tier"]
|
||||
FE["FE nginx TLS"]
|
||||
LB["L4/L7 LB optional"]
|
||||
end
|
||||
|
||||
subgraph App["Application tier"]
|
||||
WEB["Web + consoles + OTA metadata"]
|
||||
API["Control APIs PHP"]
|
||||
GIT["Gitea"]
|
||||
SHORT["URL shortener"]
|
||||
BUILD["Build / Docker"]
|
||||
end
|
||||
|
||||
subgraph Data["Data tier"]
|
||||
SQL["MariaDB"]
|
||||
OBJ["Object store / recordings"]
|
||||
end
|
||||
|
||||
subgraph Realtime["Realtime tier"]
|
||||
WG["WireGuard pool"]
|
||||
RSSH["RSSH bastion"]
|
||||
SFU["SFU / MCU future"]
|
||||
FF["ffmpeg workers future"]
|
||||
end
|
||||
|
||||
FE --> LB --> WEB
|
||||
LB --> API
|
||||
WEB --> SQL
|
||||
API --> SQL
|
||||
SHORT --> SQL
|
||||
GIT --> SQL
|
||||
BUILD --> OBJ
|
||||
SFU --> FF
|
||||
SFU --> OBJ
|
||||
WG --> RSSH
|
||||
```
|
||||
|
||||
### 6.3 Reference cluster layout (prod)
|
||||
|
||||
**Phase A (minimal split)** — cost-conscious; matches draft “tighter” intent:
|
||||
|
||||
| VM role | vCPU | RAM | Disk | Services |
|
||||
|---------|------|-----|------|----------|
|
||||
| **be-web-1** | 2 | 4 GiB | 40 GiB | nginx, PHP-FPM (hub, crashes, graphs, builder UI, OTA JSON) |
|
||||
| **be-data-1** | 2 | 8 GiB | 200 GiB+ | MariaDB primary; nightly backup to OBJ |
|
||||
| **be-ops-1** | 2 | 4 GiB | 80 GiB | Gitea, url-shortener, small object cache |
|
||||
| **be-build-1** | 4 | 16 GiB | 100 GiB | Docker builder; ephemeral |
|
||||
| **be-vpn-1** | 2 | 2 GiB | 20 GiB | WireGuard + RSSH bastion |
|
||||
| **fe-1** | (existing) | — | — | Gentoo FE — unchanged |
|
||||
|
||||
**Phase B (scale-out)** — when metrics justify:
|
||||
|
||||
| Add | When |
|
||||
|-----|------|
|
||||
| **be-web-2+** | PHP-FPM CPU > 70% sustained or p95 latency SLO miss |
|
||||
| **be-vpn-2+** | RSSH session count > [§12.1](#121-rssh-remote-access-connections) per node |
|
||||
| **be-media-N** | SFU/ffmpeg SPEC approved |
|
||||
| **CDN edge** | OTA/release origin egress > [§12.3](#123-network-bandwidth-and-egress) budget |
|
||||
| **Regional PoP** | p95 RTT > 300 ms for target markets + media revenue (F1/F3) |
|
||||
| **Managed SQL** | DBA ops > 4 h/month or replica lag requirements |
|
||||
|
||||
### 6.4 Traffic and control planes
|
||||
|
||||
| Plane | Protocols | Termination | Notes |
|
||||
|-------|-----------|-------------|-------|
|
||||
| **Control** | HTTPS :443 | FE → BE :80 | Heartbeats, upload, consoles |
|
||||
| **Git** | HTTPS / SSH | FE path or dedicated | Gitea behind same FE prefix |
|
||||
| **OTA** | HTTPS static | FE → BE or CDN | Large artifacts → object store |
|
||||
| **VPN lab** | UDP :45340 | Router DNAT → FE → BE | Unchanged until WG pool |
|
||||
| **RSSH** | TCP :443 | `ra.apps.f0xx.org` | Bastion may move to dedicated VM |
|
||||
| **Cast LAN** | UDP/TCP :41234–41235 | Device-local | No BE in alpha |
|
||||
| **Cast WAN** | UDP/TURN/WebRTC | SFU tier (future) | See [20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md) §SFU |
|
||||
|
||||
---
|
||||
|
||||
## 7. Service inventory and scaling profile
|
||||
|
||||
| Service | Stateful? | Scale axis | Co-location rule |
|
||||
|---------|-----------|------------|------------------|
|
||||
| Hub / crashes / tickets | No (PHP) | Horizontal | With API pool |
|
||||
| Graphs ingest + UI | No | Horizontal | Same pool |
|
||||
| Builder UI | No | Horizontal | Same pool; triggers build VM |
|
||||
| Docker build | Ephemeral | Queue workers | **Dedicated** CPU VM |
|
||||
| MariaDB | Yes | Vertical → replica | **Dedicated**; no ffmpeg |
|
||||
| Gitea | Yes (git) | Vertical; mirror RO | Low traffic; ops VM OK |
|
||||
| URL shortener | No | Horizontal | Small; can share ops VM |
|
||||
| OTA artifacts | Yes (files) | CDN / object store | Split from PHP |
|
||||
| WireGuard | Yes (peers) | Pool by region | UDP-friendly VM |
|
||||
| RSSH bastion | Session | Horizontal + sticky | See §12.1 |
|
||||
| SFU / ffmpeg | Session | Horizontal | **Dedicated** media subnet |
|
||||
|
||||
---
|
||||
|
||||
## 8. Deployment and promotion pipeline
|
||||
|
||||
```text
|
||||
feature/* or next ──► dev cluster (auto or manual)
|
||||
│
|
||||
▼ QA + PO
|
||||
staging cluster
|
||||
│
|
||||
▼ soak + sign-off ([ALPHA.md](../ALPHA.md) pattern extended)
|
||||
prod cluster
|
||||
```
|
||||
|
||||
| Step | Actor | Gate |
|
||||
|------|-------|------|
|
||||
| Deploy to **dev** | DEV / CI | Unit tests green |
|
||||
| Promote to **staging** | PO or release delegate | Integration + e2e smoke |
|
||||
| Promote to **prod** | PO | Staging soak; [GIT_FLOW.md](../GIT_FLOW.md) tag on `master` for app releases |
|
||||
|
||||
**Config rule:** `config.php` and secrets are **environment-specific**; post-sync scripts (e.g. `ensure_url_shortener_prod_config.php`) must be idempotent per environment.
|
||||
|
||||
---
|
||||
|
||||
## 9. Zero-downtime and load balancing
|
||||
|
||||
| Tier | Strategy |
|
||||
|------|----------|
|
||||
| **PHP-FPM** | N≥2 VMs behind FE upstream block or cloud LB; `max_fpm_children` tuned per VM |
|
||||
| **MariaDB** | Primary + read replica; app read/write split optional later |
|
||||
| **Gitea** | Single primary; maintenance window for upgrades unless Gitea HA adopted |
|
||||
| **WireGuard** | New peers on least-loaded node; existing peers sticky until session end |
|
||||
| **Static OTA** | Blue bucket + CDN swap or versioned path (`v0/ota/…`) |
|
||||
|
||||
**FE nginx pattern (multi-BE):**
|
||||
|
||||
```nginx
|
||||
upstream androidcast_be {
|
||||
least_conn;
|
||||
server 10.7.16.128:80 max_fails=2 fail_timeout=10s;
|
||||
server 10.7.16.129:80 max_fails=2 fail_timeout=10s;
|
||||
}
|
||||
```
|
||||
|
||||
Cloud providers: prefer **managed LB** (GCP ILB, Azure Front Door/Application Gateway) when FE moves partial traffic to cloud.
|
||||
|
||||
---
|
||||
|
||||
## 10. Data and storage
|
||||
|
||||
| Data class | Store | Backup |
|
||||
|------------|-------|--------|
|
||||
| Relational (crashes, tickets, RA, short links) | MariaDB | Daily logical dump + binlog optional |
|
||||
| Git mirrors | Gitea disk | `gitea_collect_backup.sh`; off-VM copy |
|
||||
| OTA / APK / build artifacts | Object store or large volume | Version immutability |
|
||||
| Cast recordings (future) | Object store + lifecycle policy | Glacier-class after N days |
|
||||
| Audit / graphs time series | MariaDB or TSDB later | DR §open |
|
||||
|
||||
**Rule:** Object store VMs optimize for **disk IOPS and egress**, not CPU.
|
||||
|
||||
---
|
||||
|
||||
## 11. Simulation lab (Alpine HVMs)
|
||||
|
||||
Sysop MAY simulate a cluster on FE hypervisor:
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Base OS | Alpine 3.x (match prod BE) |
|
||||
| Pre-install | `nginx`, `php81`, `php81-fpm`, `mariadb`, `git`, `docker`, `wireguard-tools` |
|
||||
| Network | Private `10.7.0.0/8` — **no WAN topology change** |
|
||||
| Count | 3–7 HVMs typical for dev cluster |
|
||||
|
||||
```text
|
||||
FE XEN
|
||||
├── hvm-be-web-dev
|
||||
├── hvm-be-data-dev
|
||||
├── hvm-be-ops-dev
|
||||
└── hvm-be-build-dev
|
||||
```
|
||||
|
||||
**Deliverable (future script):** `orchestration/sim/cluster-up.sh` — out of scope until PO prioritizes; SPEC reserves name.
|
||||
|
||||
---
|
||||
|
||||
## 12. Capacity planning framework
|
||||
|
||||
### 12.1 RSSH / remote access connections
|
||||
|
||||
Planning numbers for **single bastion VM** (2 vCPU, 2 GiB RAM, Alpine):
|
||||
|
||||
| Metric | Conservative | Target design |
|
||||
|--------|--------------|---------------|
|
||||
| Concurrent **SSH** sessions | 50 | 100 with `MaxSessions` tuning |
|
||||
| Concurrent **heartbeat** devices (poll 1–7 min) | 2 000 | 5 000 (mostly idle HTTP) |
|
||||
| Peak **new sessions / min** | 10 | 30 with queue |
|
||||
| WireGuard **peers** (lab track) | 200 | 500 split across 2 WG nodes |
|
||||
|
||||
**Bottleneck order:** MariaDB connection pool → sshd `MaxStartups` → PHP-FPM for heartbeat API → CPU on bastion.
|
||||
|
||||
When **staging** exceeds conservative column for 24 h, add **be-vpn-2** before cloud migration.
|
||||
|
||||
### 12.2 Cast and media paths (future)
|
||||
|
||||
| Mode | Latency | Scale unit | ffmpeg role |
|
||||
|------|---------|------------|-------------|
|
||||
| LAN P2P | Lowest | N/A (alpha) | None |
|
||||
| SFU relay | Low | 1 SFU ≈ 50–200 viewers (codec dependent) | Optional simulcast |
|
||||
| HLS/m3u8 bridge | Medium–high | 1 worker ≈ 10–30 transcodes | Heavy CPU |
|
||||
|
||||
Detailed SFU sizing deferred to owner SFU SPEC ([OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md)).
|
||||
|
||||
### 12.3 Network bandwidth and egress
|
||||
|
||||
Scale planning MUST include **throughput**, not only CPU/RAM/connections. Hidden bottleneck today: **single FE WAN uplink** → **FE→BE backhaul** → **single BE NIC**.
|
||||
|
||||
#### Link inventory (baseline)
|
||||
|
||||
| Link | Typical role | Metric |
|
||||
|------|--------------|--------|
|
||||
| **Router WAN** (`134.17.26.161`) | Public ingress/egress | Mbps sustained + peak; GB/month |
|
||||
| **FE ↔ BE** (`10.7.0.10` → `10.7.16.128`) | Proxied HTTP(S), internal sync | Mbps; must exceed sum of BE-facing services |
|
||||
| **BE origin** | PHP, MariaDB, Gitea, OTA origin | NIC util %; egress GB/month |
|
||||
| **UDP DNAT** | WireGuard lab | Mbps per active peer × peers |
|
||||
| **Future CDN origin pull** | OTA, recordings, HLS | Origin Mbps; **CDN egress** billed separately |
|
||||
|
||||
#### Per-service bandwidth profile (planning)
|
||||
|
||||
| Service | Direction | Order of magnitude (prod planning) | Scale trigger |
|
||||
|---------|-----------|-----------------------------------|---------------|
|
||||
| Control APIs (upload, heartbeat) | In ≪ out | 1–20 Mbps peak; low GB/month | p95 latency, not Mbps |
|
||||
| Consoles / hub | Out | < 5 Mbps sustained | CDN cache static assets |
|
||||
| OTA APK / `.otapkg` | Out | **Burst 50–500 Mbps** during release | CDN **required** before wide rollout |
|
||||
| Gitea clone/fetch | Out | 10–100 Mbps burst | Mirror geo cache |
|
||||
| URL shortener redirect | Out | Low | — |
|
||||
| Cast recordings (F3) | Out | **100 Mbps–1 Gbps** per hot object | Object store + CDN; geo replicas |
|
||||
| SFU relay (F1) | In+out symmetric | **5–50 Mbps × streams** | Dedicated media NIC / region |
|
||||
| ffmpeg transcode (F2) | In+out | **10–100 Mbps** per job | Worker pool + queue depth |
|
||||
| Commercial VPN (F4) | UDP bidirectional | **1–5 Mbps × active tunnels** | WG pool per region |
|
||||
|
||||
#### Budget template (PO fills per environment)
|
||||
|
||||
| Metric | Dev | Staging | Prod (initial) | Alert |
|
||||
|--------|-----|---------|----------------|-------|
|
||||
| WAN egress GB/month | — | — | _TBD_ | 80% of ISP/cloud quota |
|
||||
| Peak WAN Mbps | — | — | _TBD_ | 70% of measured cap |
|
||||
| FE→BE peak Mbps | — | — | _TBD_ | sustained > 60% 15 min |
|
||||
| OTA release peak | — | soak test | CDN absorbs > 90% | origin > 100 Mbps |
|
||||
| Media egress GB/month | — | — | _TBD_ (F3+) | cost budget |
|
||||
|
||||
**DR rule:** Do not add **be-web-2** for bandwidth until CDN/OTA offload is evaluated — horizontal PHP nodes do not fix a saturated WAN link.
|
||||
|
||||
---
|
||||
|
||||
## 13. Cloud migration (GCP / Azure / other)
|
||||
|
||||
**Hybrid recommended** for first cloud phase:
|
||||
|
||||
| Stay on-prem (FE + latency-sensitive) | Cloud candidate |
|
||||
|---------------------------------------|-----------------|
|
||||
| Gentoo FE TLS (or parallel cloud LB) | Object store (GCS/Azure Blob) |
|
||||
| WireGuard UDP if DNAT chain kept | Managed MariaDB |
|
||||
| LAN cast | ffmpeg worker MIG / VMSS |
|
||||
| | SFU pool (GPU optional) |
|
||||
|
||||
**Migration sequence (normative):**
|
||||
|
||||
1. Object store for OTA + build artifacts
|
||||
2. Read-only analytics replica or backup target
|
||||
3. Staging cluster entirely in cloud (dress rehearsal)
|
||||
4. Prod PHP tier behind hybrid LB
|
||||
5. Media plane last (highest risk)
|
||||
|
||||
**Egress rule:** Keep **one** canonical public URL per service; DNS/CNAME moves — no app hard-coded IP changes.
|
||||
|
||||
**Bandwidth rule:** Model **cloud egress pricing** in phase 1 object-store migration; prefer **CDN in front of origin** for any artifact > 1 MB served to > 100 users.
|
||||
|
||||
---
|
||||
|
||||
## 14. Global availability and GEO policy
|
||||
|
||||
Draft intent: **anyone in the world can use the platform** (latency permitting), while **specific content or features may be excluded by region** — e.g. a recording or OTA channel must **not** be published or downloadable in **Zambia (`ZM`)** when PO sets that policy.
|
||||
|
||||
### 14.1 Reach model
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Users["Global users"]
|
||||
U1["Americas"]
|
||||
U2["Europe / Africa"]
|
||||
U3["Asia-Pacific"]
|
||||
end
|
||||
|
||||
subgraph Edge["Edge layer"]
|
||||
DNS["GeoDNS / Anycast DNS"]
|
||||
CDN["CDN + WAF geo rules"]
|
||||
FE0["Primary FE TLS — on-prem"]
|
||||
end
|
||||
|
||||
subgraph Origin["Origin cluster(s)"]
|
||||
BE["BE VMs / cloud origin"]
|
||||
OBJ["Object store"]
|
||||
MEDIA["SFU / media PoP future"]
|
||||
end
|
||||
|
||||
U1 --> DNS
|
||||
U2 --> DNS
|
||||
U3 --> DNS
|
||||
DNS --> CDN
|
||||
CDN --> FE0
|
||||
CDN --> OBJ
|
||||
FE0 --> BE
|
||||
BE --> OBJ
|
||||
MEDIA --> OBJ
|
||||
```
|
||||
|
||||
| Phase | Reach | Latency target (control plane) |
|
||||
|-------|-------|--------------------------------|
|
||||
| **Now** | Single FE/BE (EU-centric) | Best-effort worldwide HTTPS |
|
||||
| **Phase 5–6** | CDN for static + OTA | p95 < 300 ms TTFB via edge |
|
||||
| **Future** | Optional regional **media PoP** + signaling | RT cast < 150 ms LAN-equivalent where PoP exists |
|
||||
|
||||
**Normative:** Public APIs and consoles remain **globally reachable** unless PO declares a **maintenance** or **sanctions** block at edge (separate from per-content deny).
|
||||
|
||||
### 14.2 Segment isolation
|
||||
|
||||
| Concept | Definition |
|
||||
|---------|------------|
|
||||
| **Segment** | ISO country code, optional region group (`EU`, `AFRICA`, custom list) |
|
||||
| **Default** | **Allow serve** worldwide for control plane; content inherits tenant default |
|
||||
| **deny_publish** | Content must not be **made available** in listed segments (upload/mirror/index) |
|
||||
| **deny_serve** | Existing object must not be **delivered** (HTTP 451 / empty manifest / VPN reject) |
|
||||
| **allow_only** | Exclusive list — only these segments (stricter catalogues) |
|
||||
|
||||
**Example (illustrative):**
|
||||
|
||||
```json
|
||||
{
|
||||
"resource": "cast_recording:abc123",
|
||||
"geo_policy": {
|
||||
"default": "allow",
|
||||
"deny_serve": ["ZM"],
|
||||
"deny_publish": ["ZM"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Same pattern applies to OTA channels, short-link targets (optional), and paid stream entitlements (F5).
|
||||
|
||||
| Content class | Typical policy |
|
||||
|---------------|----------------|
|
||||
| Crash/ticket consoles | Global (operator VPN/RBAC unchanged) |
|
||||
| OTA `stable` channel | Global unless PO restricts beta regions |
|
||||
| Cast recording (F3) | **Per-object** deny segments |
|
||||
| HLS rebroadcast (F9) | Per-channel geo + copyright rules |
|
||||
| Commercial VPN exit (F4) | **egress region** choice, not arbitrary bypass of deny_serve |
|
||||
|
||||
### 14.3 Enforcement layers
|
||||
|
||||
| Layer | Mechanism | When |
|
||||
|-------|-----------|------|
|
||||
| **L1 DNS / GeoDNS** | Route to nearest healthy edge | Multi-PoP phase |
|
||||
| **L2 CDN / WAF** | Geo block or cache vary-by-country | OTA, static, recording download |
|
||||
| **L3 FE nginx** | `geo` module or auth subrequest | Optional coarse blocks |
|
||||
| **L4 BE API** | Resolve client country (CDN header `CF-IPCountry`, GeoIP) → **geo_policy** check | Normative for dynamic content |
|
||||
| **L5 Object store** | Bucket policy / signed URL with segment claim | Large artifacts |
|
||||
| **L6 App / entitlements** | Mobile checks BE before playback | F3, F5, F9 |
|
||||
|
||||
**Headers (normative when behind CDN):** trust `X-Geo-Country` or provider equivalent only from **FE/CDN** — strip at edge from client.
|
||||
|
||||
**Audit:** deny_serve events logged to audit table (who, resource, country, time) for operator review.
|
||||
|
||||
**Dev/staging:** may use `X-Debug-Country: ZM` header to simulate segments without VPN.
|
||||
|
||||
---
|
||||
|
||||
## 15. RBAC and operator model evolution
|
||||
|
||||
| Phase | Model |
|
||||
|-------|-------|
|
||||
| **Now** | Developer-centric; shared `ac_crash_sess`; privilege sets for tickets, RA, short links |
|
||||
| **Staging scale** | Separate **operator** vs **developer** orgs in Gitea; audit log export |
|
||||
| **Prod commercial** | Tenant-scoped RBAC; customer admin roles; SSO optional |
|
||||
|
||||
Platform MUST support **per-environment** user tables or row-level `environment_id` before multi-tenant commercial features (F5–F9).
|
||||
|
||||
---
|
||||
|
||||
## 16. Commercial and paid services (platform hooks)
|
||||
|
||||
No implementation in this SPEC — only **interfaces**:
|
||||
|
||||
| Hook | Requirement |
|
||||
|------|-------------|
|
||||
| **Billing** | Webhook idempotency; `tenant_id` on all paid resources |
|
||||
| **Metering** | VPN minutes, storage GB, transcode minutes, **egress GB** — export to metrics |
|
||||
| **Entitlements** | Mobile app checks BE `/api/v1/entitlements` (future); includes **geo_allowed** flag |
|
||||
| **Add-ons** | Feature flags per user; GPU queue for masks/background |
|
||||
|
||||
Payment provider SHOULD have **no monthly minimum** at low volume (draft: “near 0 for developer”).
|
||||
|
||||
---
|
||||
|
||||
## 17. Observability and SLO
|
||||
|
||||
| Service | Health | SLO (prod, initial) |
|
||||
|---------|--------|---------------------|
|
||||
| Control APIs | `/api/…` + upload smoke | 99.5% monthly |
|
||||
| Consoles | HTTP 200 login page | 99.5% |
|
||||
| URL shortener | `/api/v1/health` | 99.9% |
|
||||
| RSSH heartbeat | device poll success rate | 99% |
|
||||
| Gitea | mirror sync age < 2× interval | Best effort |
|
||||
|
||||
**Bandwidth metrics (add to graphs / future central metrics):**
|
||||
|
||||
| Metric | Source | Alert |
|
||||
|--------|--------|-------|
|
||||
| `wan_egress_mbps` | router or FE interface | > 70% cap 15 min |
|
||||
| `fe_be_backhaul_mbps` | FE→BE link | > 60% cap |
|
||||
| `origin_egress_gbytes_month` | nginx / object store | > 80% budget |
|
||||
| `cdn_cache_hit_ratio` | CDN analytics | < 85% during OTA |
|
||||
| `geo_deny_serve_total` | BE audit | anomaly spike |
|
||||
| `sfu_media_mbps` | SFU (future) | per-PoP capacity plan |
|
||||
|
||||
Use existing graphs console + future centralized metrics ([GRAFANA_vs_others_graphvis_pivot.md](../GRAFANA_vs_others_graphvis_pivot.md)).
|
||||
|
||||
---
|
||||
|
||||
## 18. Safety constraints (current topology)
|
||||
|
||||
From [bottomline_reminder.txt](../../bottomline_reminder.txt) and [INFRA.md](../INFRA.md):
|
||||
|
||||
| Rule | Detail |
|
||||
|------|--------|
|
||||
| **No risky WAN changes** | Router `134.17.26.161` → FE `10.7.0.10` chain stays until DR-approved migration |
|
||||
| **Port 8089** | Janus/other — not androidcast vhost |
|
||||
| **BE listen 80** | Canonical for FE `proxy_pass` |
|
||||
| **ssh access** | `ssh alpine-be`, `ssh f0xx-monstro`, `ssh f0xx.org` — inspect `tmp/FE_gentoo`, `tmp/BE_alpine` before infra edits |
|
||||
| **Simulation only** | New HVMs on intra `10.7.0.0/8`; no public IP experiments without PO |
|
||||
|
||||
---
|
||||
|
||||
## 19. Phased delivery map
|
||||
|
||||
| Phase | Scope | Alpha relation |
|
||||
|-------|-------|----------------|
|
||||
| **0** | Document + simulation design (this SPEC/DR) | Parallel to alpha |
|
||||
| **1** | Split **build** Docker to dedicated VM; OTA on object disk | Post-alpha |
|
||||
| **2** | **be-web-2** + FE upstream; staging cluster on HVMs | Post-alpha |
|
||||
| **3** | Managed backup; MariaDB replica | Post-alpha |
|
||||
| **4** | SFU + ffmpeg workers (separate SPEC) | Post-alpha |
|
||||
| **5** | Hybrid cloud staging + **CDN for OTA/static** | Strategic |
|
||||
| **5b** | **Bandwidth dashboards** + egress budgets on graphs | Strategic |
|
||||
| **6** | Commercial hooks F5–F9 | Product-dependent |
|
||||
| **7** | **GEO policy** enforcement (F10) for recordings/OTA/catalog | After F3 or PO mandate |
|
||||
|
||||
---
|
||||
|
||||
## 20. Out of scope (this SPEC revision)
|
||||
|
||||
- Concrete Terraform/Pulumi modules
|
||||
- Pricing quotes for GCP/Azure
|
||||
- Legal/compliance for payments and VPN resale
|
||||
- Mobile app changes for commercial features
|
||||
- Replacing MariaDB with Postgres
|
||||
- Sanctions-law mapping per country — PO/legal owns lists; platform stores **deny segments** only
|
||||
|
||||
---
|
||||
|
||||
## 21. Related docs
|
||||
|
||||
| Doc | Link |
|
||||
|-----|------|
|
||||
| Infra topology | [INFRA.md](../INFRA.md) |
|
||||
| BE service map | [20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md) |
|
||||
| Build / Docker | [BUILD_DEPLOY.md](../BUILD_DEPLOY.md) |
|
||||
| Remote access | [REMOTE_ACCESS_IMPL.md](../REMOTE_ACCESS_IMPL.md), [20260602_REVERSE_SSH_proposals_summary.md](../20260602_REVERSE_SSH_proposals_summary.md) |
|
||||
| Open tasks graph | [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md) |
|
||||
| Alpha priorities | [20260608_ALPHA_PRIORITIES.md](../20260608_ALPHA_PRIORITIES.md) |
|
||||
| URL shortener | [specs/20100611_3_url_shortener.md](20100611_3_url_shortener.md) |
|
||||
| Design review | [DRs/20100612_1_scaling.md](../DRs/20100612_1_scaling.md) |
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-06-12 | Initial SPEC from draft `20100612_1_scaling.txt` |
|
||||
| 2026-06-12 | Rev. 2 — network bandwidth (§12.3), global availability + GEO policy (§14), metrics |
|
||||
874
specs/20100612_1_scaling.pdf
Normal file
874
specs/20100612_1_scaling.pdf
Normal file
File diff suppressed because one or more lines are too long
1084
specs/20260618_repos_reorganizing.md
Normal file
1084
specs/20260618_repos_reorganizing.md
Normal file
File diff suppressed because it is too large
Load Diff
1121
specs/20260618_repos_reorganizing.pdf
Normal file
1121
specs/20260618_repos_reorganizing.pdf
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user