{
  "openapi": "3.1.0",
  "info": {
    "title": "NodePier Agent API",
    "version": "2.0.0",
    "description": "Single-owner static hosting API. Authentication requires an ECDSA P-256 owner-key signature followed by a TOTP code. Domain ownership TXT challenges are deliberately not used."
  },
  "servers": [{ "url": "/", "description": "Current NodePier instance" }],
  "x-nodepier-auth-workflow": [
    "POST /api/auth/challenge with the authorized public JWK.",
    "Sign the returned message using ECDSA P-256 and SHA-256.",
    "POST /api/auth/verify with the challenge ID and base64url signature.",
    "On first use, either call /api/auth/totp/skip for key-only access or enroll with /api/auth/totp/setup and /api/auth/totp/confirm. If enrolled, call /api/auth/totp/verify with a current six-digit TOTP code on later sign-ins.",
    "Use the returned console token as Authorization: Bearer <token>."
  ],
  "paths": {
    "/api/config": {
      "get": {
        "operationId": "getNodePierConfig",
        "summary": "Read public server capabilities",
        "responses": { "200": { "description": "Configuration", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Config" } } } } }
      }
    },
    "/api/auth/challenge": {
      "post": {
        "operationId": "createOwnerChallenge",
        "summary": "Create a one-time owner-key signing challenge",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["publicKey"], "properties": { "publicKey": { "$ref": "#/components/schemas/PublicJwk" } } } } } },
        "responses": {
          "200": { "description": "Challenge created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Challenge" } } } },
          "403": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/auth/verify": {
      "post": {
        "operationId": "verifyOwnerSignature",
        "summary": "Verify the owner signature and begin the TOTP step",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["challengeId", "signature"], "properties": { "challengeId": { "type": "string", "format": "uuid" }, "signature": { "type": "string", "description": "Base64url ECDSA signature" } } } } } },
        "responses": { "200": { "description": "Key proof accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PreAuth" } } } }, "401": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/api/auth/totp/setup": {
      "post": {
        "operationId": "beginTotpEnrollment",
        "summary": "Create the first authenticator enrollment",
        "description": "Available only when TOTP is not configured. Use the preAuthToken returned by signature verification.",
        "security": [{ "preAuth": [] }],
        "responses": { "200": { "description": "Authenticator setup details", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpSetup" } } } }, "409": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/api/auth/totp/confirm": {
      "post": {
        "operationId": "confirmTotpEnrollment",
        "summary": "Confirm first-time TOTP enrollment and obtain a console token",
        "security": [{ "preAuth": [] }],
        "requestBody": { "$ref": "#/components/requestBodies/TotpCode" },
        "responses": { "200": { "$ref": "#/components/responses/Session" }, "401": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/api/auth/totp/skip": {
      "post": {
        "operationId": "skipTotpEnrollment",
        "summary": "Continue with key-only authentication while TOTP is not configured",
        "description": "Two-factor enrollment is optional. This route cannot be used after TOTP has been enabled.",
        "security": [{ "preAuth": [] }],
        "responses": { "200": { "$ref": "#/components/responses/Session" }, "409": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/api/auth/totp/verify": {
      "post": {
        "operationId": "verifyTotp",
        "summary": "Verify a current TOTP code and obtain a console token",
        "security": [{ "preAuth": [] }],
        "requestBody": { "$ref": "#/components/requestBodies/TotpCode" },
        "responses": { "200": { "$ref": "#/components/responses/Session" }, "401": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/api/auth/session": {
      "get": {
        "operationId": "getSession",
        "summary": "Validate a console session",
        "security": [{ "consoleAuth": [] }],
        "responses": { "200": { "description": "Current owner session" }, "401": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/api/sites": {
      "get": {
        "operationId": "listSites",
        "summary": "List every deployed and draft site",
        "security": [{ "consoleAuth": [] }],
        "responses": { "200": { "description": "Site list", "content": { "application/json": { "schema": { "type": "object", "required": ["sites"], "properties": { "sites": { "type": "array", "items": { "$ref": "#/components/schemas/Site" } } } } } } } }
      },
      "post": {
        "operationId": "createSite",
        "summary": "Reserve a NodePier subdomain or custom domain",
        "description": "For addressType=subdomain, omit domain to generate one or send a value returned by /api/sites/suggest. For addressType=custom, provide the full domain. No ownership TXT proof is required.",
        "security": [{ "consoleAuth": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSite" } } } },
        "responses": { "201": { "$ref": "#/components/responses/Site" }, "409": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/api/sites/suggest": {
      "get": {
        "operationId": "suggestSubdomain",
        "summary": "Generate an available random nodepier.com subdomain",
        "security": [{ "consoleAuth": [] }],
        "responses": { "200": { "description": "Suggestion", "content": { "application/json": { "schema": { "type": "object", "required": ["domain"], "properties": { "domain": { "type": "string", "example": "quiet-beacon-a31.nodepier.com" } } } } } } }
      }
    },
    "/api/sites/{domain}": {
      "parameters": [{ "$ref": "#/components/parameters/Domain" }],
      "get": {
        "operationId": "getSite",
        "summary": "Get one site",
        "security": [{ "consoleAuth": [] }],
        "responses": { "200": { "$ref": "#/components/responses/Site" }, "404": { "$ref": "#/components/responses/Error" } }
      },
      "delete": {
        "operationId": "removeSite",
        "summary": "Permanently remove a site and its stored releases",
        "security": [{ "consoleAuth": [] }],
        "responses": { "204": { "description": "Removed" }, "404": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/api/sites/{domain}/deploy": {
      "parameters": [{ "$ref": "#/components/parameters/Domain" }],
      "post": {
        "operationId": "deploySite",
        "summary": "Upload a complete atomic site release",
        "security": [{ "consoleAuth": [] }],
        "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "required": ["manifest", "files"], "properties": { "manifest": { "type": "string", "description": "JSON array of safe relative paths in the same order as files" }, "spa": { "type": "boolean", "default": false }, "files": { "type": "array", "items": { "type": "string", "format": "binary" } } } } } } },
        "responses": { "201": { "$ref": "#/components/responses/Site" }, "413": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/api/sites/{domain}/analytics": {
      "parameters": [{ "$ref": "#/components/parameters/Domain" }],
      "get": {
        "operationId": "getSiteAnalytics",
        "summary": "Read private traffic analytics for one site",
        "description": "Returns aggregated Nginx traffic for the selected 24-hour, 7-day, or 30-day range. Unique visitors are approximate, based on IP address, and obvious bots are excluded. Raw IP addresses and user agents are never returned.",
        "security": [{ "consoleAuth": [] }],
        "parameters": [{ "name": "range", "in": "query", "schema": { "enum": ["24h", "7d", "30d"], "default": "7d" } }],
        "responses": {
          "200": { "description": "Aggregated site analytics", "content": { "application/json": { "schema": { "type": "object", "required": ["analytics"], "properties": { "analytics": { "$ref": "#/components/schemas/Analytics" } } } } } },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/api/sites/{domain}/activate": {
      "parameters": [{ "$ref": "#/components/parameters/Domain" }],
      "post": { "operationId": "activateSite", "summary": "Check routing and enable Nginx plus TLS", "security": [{ "consoleAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/Site" }, "409": { "$ref": "#/components/responses/Error" } } }
    },
    "/api/sites/{domain}/stop": {
      "parameters": [{ "$ref": "#/components/parameters/Domain" }],
      "post": { "operationId": "stopSite", "summary": "Temporarily stop serving a site", "security": [{ "consoleAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/Site" } } }
    },
    "/api/sites/{domain}/start": {
      "parameters": [{ "$ref": "#/components/parameters/Domain" }],
      "post": { "operationId": "startSite", "summary": "Resume a stopped site", "security": [{ "consoleAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/Site" } } }
    }
  },
  "components": {
    "securitySchemes": {
      "preAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "NodePier pre-auth proof" },
      "consoleAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "NodePier console session" }
    },
    "parameters": {
      "Domain": { "name": "domain", "in": "path", "required": true, "schema": { "type": "string", "format": "hostname" } }
    },
    "requestBodies": {
      "TotpCode": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["code"], "properties": { "code": { "type": "string", "pattern": "^[0-9]{6}$", "example": "123456" } } } } } }
    },
    "responses": {
      "Error": { "description": "NodePier error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Session": { "description": "Console session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } },
      "Site": { "description": "Site state", "content": { "application/json": { "schema": { "type": "object", "required": ["site"], "properties": { "site": { "$ref": "#/components/schemas/Site" }, "checks": { "type": "array", "items": { "type": "object" } }, "provisioned": { "type": "object" } } } } } }
    },
    "schemas": {
      "PublicJwk": { "type": "object", "required": ["kty", "crv", "x", "y"], "properties": { "kty": { "const": "EC" }, "crv": { "const": "P-256" }, "x": { "type": "string" }, "y": { "type": "string" } } },
      "Challenge": { "type": "object", "required": ["challengeId", "message", "expiresAt", "fingerprint"], "properties": { "challengeId": { "type": "string", "format": "uuid" }, "message": { "type": "string" }, "expiresAt": { "type": "string", "format": "date-time" }, "fingerprint": { "type": "string" } } },
      "PreAuth": { "type": "object", "required": ["fingerprint", "preAuthToken", "requiresTwoFactor", "twoFactorSetupRequired"], "properties": { "fingerprint": { "type": "string" }, "preAuthToken": { "type": "string" }, "requiresTwoFactor": { "const": true }, "twoFactorSetupRequired": { "type": "boolean" }, "expiresInSeconds": { "type": "integer" } } },
      "TotpSetup": { "type": "object", "required": ["qrCode", "secret", "issuer", "account"], "properties": { "qrCode": { "type": "string", "description": "Data URL containing a QR PNG" }, "secret": { "type": "string", "description": "Grouped Base32 fallback secret" }, "issuer": { "type": "string" }, "account": { "type": "string" } } },
      "Session": { "type": "object", "required": ["token", "expiresInSeconds"], "properties": { "token": { "type": "string" }, "expiresInSeconds": { "type": "integer" } } },
      "Config": { "type": "object", "required": ["brand", "mode", "platformDomain", "twoFactorConfigured"], "properties": { "brand": { "const": "NodePier" }, "mode": { "enum": ["dry-run", "system"] }, "platformDomain": { "type": "string" }, "maxUploadBytes": { "type": "integer" }, "maxFileBytes": { "type": "integer" }, "maxFiles": { "type": "integer" }, "tokenTtlMinutes": { "type": "integer" }, "twoFactorConfigured": { "type": "boolean" } } },
      "CreateSite": { "type": "object", "required": ["addressType"], "properties": { "name": { "type": "string", "maxLength": 60 }, "addressType": { "enum": ["subdomain", "custom"] }, "domain": { "type": "string", "format": "hostname" }, "includeWww": { "type": "boolean", "default": true }, "spa": { "type": "boolean", "default": false } } },
      "DnsRecord": { "type": "object", "required": ["type", "name", "value", "required", "purpose"], "properties": { "type": { "enum": ["A", "AAAA", "CNAME"] }, "name": { "type": "string" }, "value": { "type": "string" }, "required": { "type": "boolean" }, "purpose": { "type": "string" } } },
      "Analytics": { "type": "object", "required": ["domain", "range", "from", "to", "requests", "pageViews", "uniqueVisitors", "bandwidthBytes", "averageResponseMs", "errorRate", "botRequests", "status", "devices", "series", "topPaths", "topSources"], "properties": { "domain": { "type": "string" }, "range": { "enum": ["24h", "7d", "30d"] }, "from": { "type": "string", "format": "date-time" }, "to": { "type": "string", "format": "date-time" }, "retentionDays": { "type": "integer", "const": 30 }, "available": { "type": "boolean" }, "collectedFrom": { "type": ["string", "null"], "format": "date-time" }, "truncated": { "type": "boolean" }, "requests": { "type": "integer" }, "pageViews": { "type": "integer" }, "uniqueVisitors": { "type": "integer" }, "bandwidthBytes": { "type": "integer" }, "averageResponseMs": { "type": "number" }, "errorRate": { "type": "number" }, "botRequests": { "type": "integer" }, "status": { "type": "object", "additionalProperties": { "type": "integer" } }, "devices": { "type": "object", "additionalProperties": { "type": "integer" } }, "series": { "type": "array", "items": { "type": "object" } }, "topPaths": { "type": "array", "items": { "type": "object" } }, "topSources": { "type": "array", "items": { "type": "object" } } } },
      "Site": { "type": "object", "required": ["domain", "name", "addressType", "status", "fileCount", "totalBytes", "dnsManaged", "dnsRecords"], "properties": { "domain": { "type": "string" }, "name": { "type": "string" }, "addressType": { "enum": ["subdomain", "custom"] }, "includeWww": { "type": "boolean" }, "spa": { "type": "boolean" }, "status": { "enum": ["draft", "uploaded", "provisioning", "active", "simulated", "stopped", "error"] }, "fileCount": { "type": "integer" }, "totalBytes": { "type": "integer" }, "createdAt": { "type": ["string", "null"], "format": "date-time" }, "deployedAt": { "type": ["string", "null"], "format": "date-time" }, "activatedAt": { "type": ["string", "null"], "format": "date-time" }, "stoppedAt": { "type": ["string", "null"], "format": "date-time" }, "lastError": { "type": ["string", "null"] }, "url": { "type": ["string", "null"], "format": "uri" }, "simulated": { "type": "boolean" }, "dnsManaged": { "type": "boolean" }, "dnsRecords": { "type": "array", "items": { "$ref": "#/components/schemas/DnsRecord" } } } },
      "Error": { "type": "object", "required": ["error"], "properties": { "error": { "type": "object", "required": ["code", "message"], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "details": {} } } } }
    }
  }
}
