{
  "openapi": "3.1.0",
  "info": {
    "title": "4G Dongle Manager Account API",
    "version": "0.1.0",
    "description": "Public contract for account, multi-device usage sync, and confirmed update-email subscription. Account, SMS, and sync remain unavailable. The update-email endpoint is provider-gated and opens only while the verified sending domain and server-side credentials are active."
  },
  "servers": [
    {
      "url": "https://brbrhu.com"
    }
  ],
  "x-service-status": {
    "account": "unavailable",
    "newsletter": "provider-gated"
  },
  "paths": {
    "/api/auth/email/request": {
      "post": {
        "summary": "Request an email sign-in code",
        "description": "Preferred low-cost registration and sign-in channel. Codes are server-side hashed, valid for five minutes, single-use, locked after five failed attempts, and rate-limited by email, IP, and device.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailRequest"
              }
            }
          }
        },
        "responses": {
          "503": {
            "$ref": "#/components/responses/AccountUnavailable"
          }
        }
      }
    },
    "/api/auth/email/verify": {
      "post": {
        "summary": "Verify an email code and sign in",
        "description": "The website completes this flow server-side and sets an HttpOnly, Secure, SameSite session cookie. A Mac client receives short-lived access and per-device refresh tokens and stores them in macOS Keychain.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailVerifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Future native-client response after the real account service opens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthSession"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AccountUnavailable"
          }
        }
      }
    },
    "/api/auth/sms/request": {
      "post": {
        "summary": "Request a sign-in code",
        "description": "Aliyun SMS will only be called from the server after phone, IP, and device rate limits, CAPTCHA 2.0, anti-bombing controls, and a cost ceiling are enabled.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SmsRequest"
              }
            }
          }
        },
        "responses": {
          "503": {
            "$ref": "#/components/responses/AccountUnavailable"
          }
        }
      }
    },
    "/api/auth/sms/verify": {
      "post": {
        "summary": "Verify a code and create a device session",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SmsVerifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Future success response after the real account service opens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthSession"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AccountUnavailable"
          }
        }
      }
    },
    "/api/auth/refresh": {
      "post": {
        "summary": "Rotate a per-device refresh token",
        "description": "Refresh tokens are revocable, rotated on use, grouped by device family, and stored only in macOS Keychain on the client.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Future rotated native-client session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthSession"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AccountUnavailable"
          }
        }
      }
    },
    "/api/account/phone/request": {
      "post": {
        "summary": "Request a code to bind a phone to the signed-in user",
        "description": "This authenticated flow must merge the phone into the current user_id and must never create a second user record.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PhoneRequest"
              }
            }
          }
        },
        "responses": {
          "503": {
            "$ref": "#/components/responses/AccountUnavailable"
          }
        }
      }
    },
    "/api/account/phone/verify": {
      "post": {
        "summary": "Verify and merge a phone into the signed-in user",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PhoneVerifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Future unified user response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserResponse"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AccountUnavailable"
          }
        }
      }
    },
    "/api/content/read": {
      "post": {
        "summary": "Store an authenticated content read receipt",
        "description": "Anonymous visitors keep revision IDs only in first-party browser storage. After sign-in, this endpoint upserts the same content_id for the current user_id so update reminders can stay dismissed across devices. IP addresses and device fingerprints are not identity keys.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContentReadRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Future idempotent success response."
          },
          "503": {
            "$ref": "#/components/responses/AccountUnavailable"
          }
        }
      }
    },
    "/api/usage/sync": {
      "post": {
        "summary": "Synchronize a monotonic per-device traffic counter",
        "description": "The server will upsert by account, device_id, cycle_id, and counter_id. Uploading the same cumulative value again must not duplicate usage or charges. A client that deletes local data starts a new counter_id.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UsageSyncRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Future aggregate response after the real account service opens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageSyncResponse"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AccountUnavailable"
          }
        }
      }
    },
    "/api/subscriptions/email": {
      "servers": [
        {
          "url": "https://api.brbrhu.com"
        }
      ],
      "get": {
        "summary": "Read the current update-email availability",
        "description": "The public page uses this status before enabling its form, so it does not collect an address while delivery is unavailable.",
        "responses": {
          "200": {
            "description": "Current provider-gated service status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsletterStatus"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Request a confirmed update-email subscription",
        "description": "Starts a double-opt-in flow only while the verified sending domain and server-side provider credential are active. The address is encrypted locally, indexed with a one-way hash, and not activated until the recipient confirms the email.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailSubscriptionRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "A generic confirmation response that does not reveal whether an address already exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsletterAccepted"
                }
              }
            }
          },
          "400": {
            "description": "Invalid address or missing explicit consent."
          },
          "429": {
            "description": "Daily network-address rate limit reached."
          },
          "503": {
            "$ref": "#/components/responses/NewsletterUnavailable"
          }
        }
      }
    },
    "/api/subscriptions/email/confirm": {
      "servers": [
        {
          "url": "https://api.brbrhu.com"
        }
      ],
      "get": {
        "summary": "Review an update-email confirmation",
        "description": "Displays a noindex review page. GET does not consume the token, which avoids automatic confirmation by email security scanners.",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["zh-CN", "en"] }
          }
        ],
        "responses": {
          "200": { "description": "Human review and confirmation page." },
          "400": { "description": "Invalid token." },
          "410": { "description": "Expired token." }
        }
      },
      "post": {
        "summary": "Confirm the update-email subscription",
        "description": "Consumes a valid token, activates the encrypted local subscription record, then redirects to the main site. The delivery provider is not used as a separate marketing-contact database.",
        "responses": {
          "303": { "description": "Confirmed; redirect to brbrhu.com." },
          "400": { "description": "Invalid token." },
          "410": { "description": "Expired token." },
          "503": { "$ref": "#/components/responses/NewsletterUnavailable" }
        }
      }
    },
    "/api/subscriptions/email/unsubscribe": {
      "servers": [
        {
          "url": "https://api.brbrhu.com"
        }
      ],
      "get": {
        "summary": "Review an update-email unsubscribe request",
        "description": "Displays a noindex review page. GET does not change subscription state, which avoids accidental unsubscribe by link scanners.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          },
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Human review page or already-unsubscribed result." },
          "400": { "description": "Invalid signed link." },
          "503": { "$ref": "#/components/responses/NewsletterUnavailable" }
        }
      },
      "post": {
        "summary": "Unsubscribe from update emails",
        "description": "Validates the signed action URL and marks the local subscription unsubscribed. It also accepts RFC 8058 one-click POST requests from compatible mail clients.",
        "responses": {
          "200": { "description": "Subscription is now unsubscribed." },
          "400": { "description": "Invalid signed link." },
          "503": { "$ref": "#/components/responses/NewsletterUnavailable" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "opaque access token"
      }
    },
    "responses": {
      "AccountUnavailable": {
        "description": "The account service is not open.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/AccountUnavailable"
            }
          }
        }
      },
      "NewsletterUnavailable": {
        "description": "The email subscription service is not open.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/NewsletterUnavailable"
            }
          }
        }
      }
    },
    "schemas": {
      "EmailRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["email"],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          }
        }
      },
      "EmailVerifyRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["email", "code", "device_id", "device_name"],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "code": {
            "type": "string",
            "minLength": 4,
            "maxLength": 8
          },
          "device_id": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          },
          "device_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          }
        }
      },
      "SmsRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["phone"],
        "properties": {
          "phone": {
            "type": "string",
            "pattern": "^1[3-9][0-9]{9}$"
          }
        }
      },
      "SmsVerifyRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["phone", "code", "device_id", "device_name"],
        "properties": {
          "phone": {
            "type": "string",
            "pattern": "^1[3-9][0-9]{9}$"
          },
          "code": {
            "type": "string",
            "minLength": 4,
            "maxLength": 8
          },
          "device_id": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          },
          "device_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          }
        }
      },
      "RefreshRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["refresh_token"],
        "properties": {
          "refresh_token": {
            "type": "string",
            "minLength": 32
          }
        }
      },
      "PhoneRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["phone"],
        "properties": {
          "phone": {
            "type": "string",
            "pattern": "^1[3-9][0-9]{9}$"
          }
        }
      },
      "PhoneVerifyRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["phone", "code"],
        "properties": {
          "phone": {
            "type": "string",
            "pattern": "^1[3-9][0-9]{9}$"
          },
          "code": {
            "type": "string",
            "minLength": 4,
            "maxLength": 8
          }
        }
      },
      "User": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "email", "phone"],
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": ["string", "null"],
            "format": "email"
          },
          "phone": {
            "type": ["string", "null"]
          }
        }
      },
      "UserResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["user"],
        "properties": {
          "user": {
            "$ref": "#/components/schemas/User"
          }
        }
      },
      "ContentReadRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["content_id"],
        "properties": {
          "content_id": {
            "type": "string",
            "minLength": 3,
            "maxLength": 160,
            "description": "A revision-specific public content identifier, for example intern-survival-guide:2026-07-16."
          }
        }
      },
      "AuthSession": {
        "type": "object",
        "additionalProperties": false,
        "required": ["access_token", "refresh_token", "user"],
        "properties": {
          "access_token": {
            "type": "string"
          },
          "refresh_token": {
            "type": "string"
          },
          "user": {
            "$ref": "#/components/schemas/User"
          }
        }
      },
      "UsageSyncRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "device_id",
          "device_name",
          "cycle_id",
          "counter_id",
          "received_bytes",
          "sent_bytes",
          "sampled_at"
        ],
        "properties": {
          "device_id": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          },
          "device_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "cycle_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "counter_id": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          },
          "received_bytes": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "sent_bytes": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "sampled_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "UsageSyncResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["aggregate"],
        "properties": {
          "aggregate": {
            "type": "object",
            "additionalProperties": false,
            "required": ["device_count", "cycle_received", "cycle_sent", "remaining_credits"],
            "properties": {
              "device_count": {
                "type": "integer",
                "minimum": 1
              },
              "cycle_received": {
                "type": "integer",
                "format": "int64",
                "minimum": 0
              },
              "cycle_sent": {
                "type": "integer",
                "format": "int64",
                "minimum": 0
              },
              "remaining_credits": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        }
      },
      "EmailSubscriptionRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["email", "consent"],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "consent": {
            "type": "boolean",
            "const": true
          },
          "locale": {
            "type": "string",
            "enum": ["zh-CN", "en"],
            "default": "zh-CN"
          }
        }
      },
      "NewsletterStatus": {
        "type": "object",
        "additionalProperties": false,
        "required": ["available", "mode", "sender", "reply_to"],
        "properties": {
          "available": { "type": "boolean" },
          "mode": { "const": "double_opt_in" },
          "sender": { "const": "胡律的规则探索 <letter@updates.brbrhu.com>" },
          "reply_to": { "const": "hu.zexian@qq.com" }
        }
      },
      "NewsletterAccepted": {
        "type": "object",
        "additionalProperties": false,
        "required": ["status", "message"],
        "properties": {
          "status": { "const": "confirmation_sent" },
          "message": { "type": "string" }
        }
      },
      "AccountUnavailable": {
        "type": "object",
        "additionalProperties": false,
        "required": ["error", "message", "status", "documentation_url"],
        "properties": {
          "error": {
            "const": "account_service_unavailable"
          },
          "message": {
            "const": "账户服务尚未开放"
          },
          "status": {
            "const": 503
          },
          "documentation_url": {
            "const": "https://brbrhu.com/4g-dongle-manager#account-service"
          }
        }
      },
      "NewsletterUnavailable": {
        "type": "object",
        "additionalProperties": false,
        "required": ["error", "message", "status", "documentation_url"],
        "properties": {
          "error": {
            "const": "newsletter_service_unavailable"
          },
          "message": {
            "const": "邮件订阅服务尚未开放"
          },
          "status": {
            "const": 503
          },
          "documentation_url": {
            "const": "https://brbrhu.com/#newsletter"
          }
        }
      }
    }
  }
}
