10 KiB
10 KiB
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:
- No email-ownership check at signup. Anyone can register any address.
- No published password policy. The installer cannot show or enforce the rule the server uses.
- Getting a key takes three calls (login → tokens → logout), and a full web session is handed to a desktop client.
- 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/signuptoo, for consistency), and rejects a failing password with400 {error:"weak_password", message:"…"}. - The client checks the same rule before sending (
PasswordPolicy.Check) and showsDescribe()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_BASEoverride, plusVERAE_ALLOW_INSECURE_PORTAL=1only for local tests). Server: HSTS, TLS 1.2+. - Secrets are never logged by either side. Mask request bodies on
/install/*,/login,/signup,/tokensin 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 ofauth.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, recordclient(product/version/os/host), and let users list and revoke it in the portal Security tab as usual. /install/logindoes 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_requiredis 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 |