PrintToVeraeTimeStamp/docs/installer-registration-api.md

10 KiB
Raw Blame History

Verae installer registration API (proposed)

Status: proposal + client implementation. The client side is implemented in internal/auth/portal_install.go and driven by the Windows installer (platform/windows/installer-debug.nsi → verae-print installer-account). The server side does not exist yet. The portal backend source was not available, so nothing was changed or deployed on the server. Until the server implements it, the client automatically falls back to the endpoints the portal web client already uses (see §5).

1. Why a new API

How Verae accounts work today (from https://customer.verae.ai/portal/app.js):

Today Endpoint Notes
Sign in POST /portal/api/login {email,password,totpCode?} → {token:<session>}; error totp_required when 2FA is on
Sign up POST /portal/api/signup {name,email,password} → session immediately; no email verification
Mint key POST /portal/api/tokens (Bearer session) {name,expiresInDays?} → {token:"xVerae-…"} (shown once)
Sign out POST /portal/api/logout
Validate key GET https://zap.verae.ai/v1/status with x-api-key free; 401 = invalid

Gaps for a desktop installer:

  1. No email-ownership check at signup. Anyone can register any address.
  2. No published password policy. The installer cannot show or enforce the rule the server uses.
  3. Getting a key takes three calls (login → tokens → logout), and a full web session is handed to a desktop client.
  4. No device/installer-specific rate limiting or audit field.

2. Endpoints (all under https://customer.verae.ai/portal/api)

openapi: 3.0.3
info: { title: Verae installer registration, version: 1.0.0 }
servers: [ { url: https://customer.verae.ai/portal/api } ]
paths:
  /install/capabilities:
    get:
      summary: Feature discovery + password policy (no auth)
      responses:
        "200":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Capabilities" }
        "404": { description: Not implemented → client uses legacy flow (§5) }
  /install/register:
    post:
      summary: Start account creation; emails a 6-digit code
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name, email, password]
              properties:
                name: { type: string, maxLength: 200 }
                email: { type: string, format: email }
                password: { type: string, format: password }
                client: { $ref: "#/components/schemas/Client" }
      responses:
        "202":
          description: Code sent (also returned when the email already exists, to avoid account enumeration; in that case the email says "you already have an account – sign in instead")
          content:
            application/json:
              schema:
                type: object
                properties:
                  pendingId: { type: string, description: "opaque, ≥128-bit random" }
                  expiresInSeconds: { type: integer, example: 900 }
                  resendAfterSeconds: { type: integer, example: 60 }
        "400": { $ref: "#/components/responses/Error" }  # weak_password, invalid_email, name_required
        "429": { $ref: "#/components/responses/Error" }
  /install/verify:
    post:
      summary: Confirm the code; creates the account and issues a key
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [pendingId, code]
              properties:
                pendingId: { type: string }
                code: { type: string, pattern: "^[0-9]{6}$" }
                tokenName: { type: string, example: "Verae Print Stamp - NIMO (windows)" }
      responses:
        "201":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Issued" }
        "400": { $ref: "#/components/responses/Error" }  # invalid_code (attemptsLeft), expired
        "410": { $ref: "#/components/responses/Error" }  # too many attempts / expired pendingId
  /install/resend:
    post:
      requestBody:
        content:
          application/json:
            schema: { type: object, required: [pendingId], properties: { pendingId: { type: string } } }
      responses:
        "202": { description: New code sent; previous code invalidated }
        "429": { $ref: "#/components/responses/Error" }
  /install/login:
    post:
      summary: Existing account → issue a key in one call (no web session returned)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string }
                password: { type: string, format: password }
                totpCode: { type: string }
                tokenName: { type: string }
                client: { $ref: "#/components/schemas/Client" }
      responses:
        "201":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Issued" }
        "401":
          description: "error = invalid_credentials | totp_required | totp_invalid | email_unverified"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/Error" }
components:
  responses:
    Error:
      description: Error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      properties:
        error: { type: string, description: machine code }
        message: { type: string, description: human text shown verbatim in the installer }
        attemptsLeft: { type: integer }
        retryAfterSeconds: { type: integer }
    Capabilities:
      type: object
      properties:
        installApi: { type: boolean }
        emailVerification: { type: boolean }
        passwordPolicy:
          type: object
          properties:
            minLength: { type: integer, example: 10 }
            requireLetter: { type: boolean }
            requireDigit: { type: boolean }
            requireSymbol: { type: boolean }
            disallowEmail: { type: boolean }
    Client:
      type: object
      properties:
        product: { type: string, example: verae-print }
        version: { type: string, example: 0.3.0-debug }
        os: { type: string, example: windows }
        host: { type: string }
    Issued:
      type: object
      properties:
        token: { type: string, example: "xVerae-…" }
        tokenId: { type: string }
        name: { type: string }
        expiresAt: { type: string, format: date-time, nullable: true }

3. Password policy

  • The server is the source of truth. It publishes the policy in capabilities.passwordPolicy, enforces it in /install/register (and in /signup too, for consistency), and rejects a failing password with 400 {error:"weak_password", message:"…"}.
  • The client checks the same rule before sending (PasswordPolicy.Check) and shows Describe() text on the Create page. Password and confirm must match (checked in the installer and again in the CLI).
  • Client default when the server publishes nothing: ≥10 characters, at least one letter and one digit, must not contain the email's local part. Recommend also checking against a breached-password list (k-anonymity HIBP range API) on the server.

4. Security requirements

  • HTTPS only. The client refuses non-https portal URLs (VERAE_PORTAL_BASE override, plus VERAE_ALLOW_INSECURE_PORTAL=1 only for local tests). Server: HSTS, TLS 1.2+.
  • Secrets are never logged by either side. Mask request bodies on /install/*, /login, /signup, /tokens in access logs and APM. Client: passwords are never written to any log. Keys are only logged masked (xVerae-AbC1…9xYz). The installer passes secrets to the helper only through a temp file in its private $PLUGINSDIR, which is deleted right after it is read. Secrets are never passed on a command line.
  • The password is never stored on the client. Only the issued key is saved, to <target user profile>\.verae\xverae-token (token + "\n", the format of auth.SaveXVeraeToken). The ACL is the user + SYSTEM + Administrators, with inheritance removed. If the installer is elevated with a different admin account, the key still goes to the logged-in user (owner of explorer.exe).
  • Codes: 6 digits, CSPRNG, stored hashed. TTL 15 min, 5 attempts per pendingId, resend at most once per 60 s and 5 per hour. A new code invalidates the old one.
  • Rate limits (suggested): register 5/h per IP and 3/h per email; login 10 per 15 min per IP+email, with exponential back-off; verify 5 per pendingId. Return 429 with retryAfterSeconds.
  • No enumeration: register answers 202 whether or not the email exists. Login uses one generic invalid_credentials.
  • The key is scoped and auditable: name it after tokenName, record client (product/version/os/host), and let users list and revoke it in the portal Security tab as usual.
  • /install/login does not return a session cookie or token, only the key. It does not create a web session (or revokes it immediately).

5. Client fallback (today's server)

GET /install/capabilities → 404 (or a network error) ⇒ installApi=0:

  • Existing account: POST /login → POST /tokens {name} → POST /logout. totp_required is shown to the user and the TOTP field is used.
  • New account: the password policy is checked client-side, then POST /signup → POST /tokens → POST /logout. No email code (the Create page says so). The Verify page is skipped.
  • The key is validated afterwards with GET zap.verae.ai/v1/status (free).

6. Installer UI mapping

Page Calls
Key status user-token.ps1 -Mode status (masked key + free validation), installer-account --op capabilities
Sign in installer-account --op login (email, password, optional TOTP)
Create account --op register → code_sent (Verify page) or ok (legacy)
Verify email --op verify; "Send a new code" → --op resend
Paste key button opens customer.verae.ai; validate-key before Next
Skip warning; the helper shows a key dialog on first print and holds the job until a key is saved