返回協定總覽

協定參考——API

即時呈現存放庫內的 docs/protocols/api.md。

AI LearnOS API & Protocol Specification

This document provides the authoritative reference catalog of all public, authenticated, and internal HTTP API surfaces implemented in AI LearnOS (apps/web/src/app/api/**), organized according to the master specification (§45 Protocol Surfaces).

---

1. Protocol Overview & Conventions

  • Protocol Version: 0.1 (declared in @ailearnos/identity-protocol as PROTOCOL_VERSION = "0.1").
  • Transport: HTTPS, JSON request/response bodies (except multipart form upload for source documents and URL-encoded webhook payloads from payment gateways).
  • Content-Type: application/json; charset=utf-8 (unless otherwise noted).
  • Date/Time Formatting: ISO 8601 UTC strings (YYYY-MM-DDTHH:mm:ss.sssZ).
  • Currency & Money Formatting: Integer minor units (e.g., 33000 = 330.00 TWD) per §44 money invariant.
  • Credits & Balances: Non-negative integer points per §33 credit economy invariant.

---

2. Standard Error Taxonomy

Every failed API call returns a standardized JSON error payload with an appropriate HTTP status code:

{
  "error": "machine_readable_snake_case_code",
  "message": "Human readable context (optional)",
  "details": {}
}

Standard Status Codes & Error Codes

| HTTP Status | Primary Error Code | Meaning / Invariant Trigger | |---|---|---| | 400 Bad Request | missing_param, invalid_payload, invalid_source_type, missing_product_key, invalid_assistance_kind | Missing required fields, schema validation failure, or malformed input. | | 401 Unauthorized | unauthorized, missing_auth_header, invalid_signature | Missing or invalid user session, expired session token, or HMAC mismatch. | | 403 Forbidden | forbidden, role_required, unauthorized_organization, FLAG_DISABLED | Session exists but lacks required role (requireRole), organization role (requireOrgRole), or feature flag is disabled. | | 404 Not Found | not_found, grant_expired_or_invalid, document_not_found, match_not_found | Resource does not exist, has expired, or is scoped away from the caller. | | 413 Payload Too Large | file_too_large | Uploaded document exceeds the 50MB file size limit (MAX_UPLOAD_BYTES). | | 429 Too Many Requests | rate_limited, rate_limit_exceeded | Client IP or user account exceeded rate limit quota; response includes Retry-After header. | | 500 Internal Server Error | internal_error, compile_failed, checkout_failed | Unhandled runtime exception; safe error message returned without leaking server stack traces. |

---

3. API Catalog by §45 Specification Group

3.1 Verification Surface

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/verify/[id]</code>
  • Purpose: Public verification of credential, passport claim, or proof-of-skill. Open to external viewers, employers, and verifiers without login.
  • Authentication: None (Public).
  • Path Parameters:
  • id (string, required): The opaque publicVerificationId issued to the credential.
  • Response (200 OK):

``json { "protocolVersion": "0.1", "status": "valid", "verificationId": "pvi_01HJ...", "credentialId": "crd_01HJ...", "skillNodeId": "sk_node_ml_01", "skillName": "Transformer Attention Mechanisms", "levelClaimed": "L3_proficient", "kind": "proof_of_skill", "issuedAt": "2026-08-22T08:00:00.000Z", "expiresAt": null, "revokedAt": null, "revokeReason": null, "issuer": { "id": "iss_platform_01", "displayName": "AI LearnOS Verified Authority", "authorityStatus": "verified_issuer" }, "evidenceSummary": { "count": 4, "lastAt": "2026-08-22T07:45:00.000Z", "topSource": "real_world_challenge" }, "verifiedAt": "2026-08-22T10:00:00.000Z" } ``

  • Error Responses:
  • 400 Bad Request: {"error": "missing_id"}
  • 404 Not Found: {"error": "not_found", "status": "not_found"}

---

3.2 Student Educational Email Verification

Implements the Cloudflare Email Routing + SWOT inbound verification flow (§34, §48, and docs/architecture/student-email-verification.md).

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/student-verification</code>
  • Purpose: Initiates a student verification session, generating an ephemeral 128-bit cryptographic token (stored as SHA-256) and a mailto: pre-filled URL.
  • Authentication: Required (currentUserId).
  • Rate Limit: 5 requests/hour, 10 requests/day per account, maximum 3 active pending sessions.
  • Request Body: None.
  • Response (201 Created):

``json { "id": "svs_01HJ9XYZ...", "mailtoUrl": "mailto:verify+TOKEN_STRING@verify.example.com?subject=Student%20Verification&body=...", "expiresAt": "2026-08-22T10:15:00.000Z" } ``

  • Error Responses:
  • 401 Unauthorized: {"error": "Unauthorized"}
  • 429 Too Many Requests: {"error": "Rate limit exceeded (5 requests/hour)"}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/student-verification/[id]/status</code>
  • Purpose: Polls verification status for a specific student verification session owned by the authenticated user.
  • Authentication: Required (currentUserId).
  • Path Parameters:
  • id (string, required): Session ID.
  • Response (200 OK):

``json { "sessionId": "svs_01HJ9XYZ...", "status": "verified", "expiresAt": "2026-08-22T10:15:00.000Z", "verification": { "level": "educational_email_verified", "institutionName": "National Taiwan University", "domain": "ntu.edu.tw", "verifiedAt": "2026-08-22T10:05:00.000Z", "expiresAt": "2027-08-22T10:05:00.000Z" } } ``

  • Error Responses:
  • 401 Unauthorized: {"error": "Unauthorized"}
  • 403 Forbidden: {"error": "Forbidden"} (user does not own the session)
  • 404 Not Found: {"error": "Session not found"}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/student-verification/[id]/cancel</code>
  • Purpose: Explicitly cancels an active pending student verification session.
  • Authentication: Required (currentUserId).
  • Response (200 OK): {"success": true}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/student-verification/[id]/retry</code>
  • Purpose: Cancels the previous session and generates a fresh token and mailto: link.
  • Authentication: Required (currentUserId).
  • Response (201 Created): Same shape as POST /api/student-verification.

---

3.3 Share Surface

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/share</code>
  • Purpose: Generates a privacy-preserving, scoped, time-bounded or view-bounded Share Grant for a learner's passport.
  • Authentication: Required (currentUserId).
  • Request Body:

``json { "scope": { "fullPassport": false, "skillNodeIds": ["sk_node_ml_01", "sk_node_ml_02"] }, "expiresAt": "2026-09-22T00:00:00.000Z", "maxViews": 5 } ``

  • Response (201 Created):

``json { "grant": { "id": "shg_01HJ...", "userId": "usr_01HJ...", "scope": { "fullPassport": false, "skillNodeIds": ["sk_node_ml_01", "sk_node_ml_02"] }, "expiresAt": "2026-09-22T00:00:00.000Z", "maxViews": 5, "viewCount": 0, "createdAt": "2026-08-22T10:00:00.000Z" } } ``

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/share</code>
  • Purpose: Resolves a public share grant link. Atomically increments viewCount and verifies that the grant is not expired, revoked, or exceeded.
  • Authentication: None (Public).
  • Query Parameters:
  • id (string, required): The share grant ID.
  • Response (200 OK):

``json { "grantId": "shg_01HJ...", "learner": { "displayName": "Alex Chen", "avatarUrl": null }, "scope": { "fullPassport": false, "skillNodeIds": ["sk_node_ml_01"] }, "skills": [ { "skillNodeId": "sk_node_ml_01", "name": "Transformer Attention Mechanisms", "level": "L3_proficient", "evidenceCount": 4, "lastVerifiedAt": "2026-08-22T07:45:00.000Z" } ], "expiresAt": "2026-09-22T00:00:00.000Z", "viewsRemaining": 4 } ``

  • Error Responses:
  • 400 Bad Request: {"error": "missing_share_id"}
  • 404 Not Found: {"error": "grant_expired_or_invalid"}

---

3.4 Enterprise Verification Standard (EVS) Surface (§24.3, §40)

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/evs</code>
  • Purpose: Learner issues a fixed-scope Enterprise Verification ID granting a specific organization permission to inspect specific skills.
  • Authentication: Required (currentUserId).
  • Request Body:

``json { "orgId": "org_enterprise_01", "skillNodeIds": ["sk_node_ml_01", "sk_node_stats_02"] } ``

  • Response (201 Created):

``json { "evs": { "id": "evs_01HJ...", "userId": "usr_01HJ...", "organizationId": "org_enterprise_01", "scopedSkillNodeIds": ["sk_node_ml_01", "sk_node_stats_02"], "revokedAt": null, "createdAt": "2026-08-22T10:00:00.000Z" } } ``

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/evs/resolve</code>
  • Purpose: Enterprise organization verifies an employee or candidate's EVS token.
  • Authentication: Required + Role Gated (requireRole("enterprise", "admin")).
  • Invariant: Unconditionally writes a verification_access_log record recording viewerOrgId, viewerUserId, evsId, and timestamp. Returns ONLY consented skills.
  • Request Body:

``json { "evsId": "evs_01HJ...", "orgId": "org_enterprise_01" } ``

  • Response (200 OK):

``json { "valid": true, "evsId": "evs_01HJ...", "learnerId": "usr_01HJ...", "organizationId": "org_enterprise_01", "evsVersion": "1.0", "issuer": { "displayName": "National AI Skills Institute", "tier": "accredited" }, "scopedSkills": [ { "skillNodeId": "sk_node_ml_01", "skillName": "Transformer Attention Mechanisms", "level": "L3_proficient", "historicalPeakLevel": "L4_advanced", "evidenceSummary": { "count": 4, "lastAt": "2026-08-22T07:45:00.000Z", "topSource": "real_world_challenge" }, "confidenceBand": "high", "lastVerifiedAt": "2026-08-22T07:45:00.000Z", "credentialValidity": true } ], "resolvedAt": "2026-08-22T10:10:00.000Z" } ``

  • EVS v1 Resolution Fields (§24.3):
  • evsVersion (string): Protocol version of the resolution payload, currently "1.0".
  • issuer (object | null): Top-level credential issuer with displayName and tier ("platform" | "accredited" | "community"); null when the credential has no issuer.
  • confidenceBand (string, per skill): Derived from the learner's evidence_confidence vector — < 0.4 = "low", < 0.7 = "medium", >= 0.7 = "high".
  • lastVerifiedAt (string | null, per skill): ISO timestamp from learner_skill_state.last_verified_at; null when never verified.
  • historicalPeakLevel (string, per skill): Highest SkillMasteryLevel ever attained for the skill, reported alongside the current level (§20 historical peak persists even when current state changes).
  • credentialValidity (boolean, per skill): true iff an active non-revoked proof-of-skill credential exists for that skill.
  • Error Responses:
  • 401 Unauthorized: {"error": "unauthorized"}
  • 403 Forbidden: {"error": "unauthorized_organization", "valid": false}
  • 404 Not Found: {"error": "evs_not_found_or_revoked", "valid": false}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/evs/revoke</code>
  • Purpose: Learner revokes an issued EVS token immediately.
  • Authentication: Required (currentUserId).
  • Request Body: {"evsId": "evs_01HJ..."}
  • Response (200 OK): {"success": true, "evs": { ... }}

---

3.5 Opportunity &amp; Network Surface (§31, §39)

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/network/leaderboard</code>
  • Purpose: Reads capability rankings across Free and Premium leaderboard zones without pay-to-win manipulation.
  • Authentication: None (Public read).
  • Query Parameters:
  • zone (string, optional, default "free"): "free" | "premium".
  • skillNodeId (string, optional): Filters rankings by specific skill.
  • limit (integer, optional, default 50, max 100).
  • Response (200 OK):

``json { "entries": [ { "rank": 1, "userId": "usr_01HJ...", "displayName": "Alex Chen", "zone": "free", "totalMasteryPoints": 1450, "skillsMastered": 12, "topSkill": "Transformer Attention Mechanisms" } ] } ``

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/network/teams</code>
  • Purpose: Learner creates, joins, or leaves a collaborative team.
  • Authentication: Required (currentUserId).
  • Request Body:

``json { "action": "create", "name": "Distributed Systems Study Group" } ``

  • Response (200 OK): {"team": { ... }}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/network/teams</code>
  • Purpose: Lists team rankings.
  • Query Parameters: zone ("free" | "premium"), limit (default 20).
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/network/matches</code>
  • Purpose: Recomputes and returns privacy-preserving opportunity matches for the authenticated learner.
  • Authentication: Required (currentUserId).
  • Response (200 OK):

``json { "matches": [ { "id": "mat_01HJ...", "companyName": "Anthropic Partner Lab", "roleTitle": "AI Systems Engineer", "matchScore": 0.92, "matchedSkills": ["Transformer Attention Mechanisms", "Postgres Optimization"], "status": "pending_learner_release", "releasedAt": null } ] } ``

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/network/matches/[id]/release</code>
  • Purpose: Learner explicitly consents to release their identity and verified passport to the hiring company for a specific matched opportunity.
  • Authentication: Required (currentUserId).
  • Path Parameters:
  • id (string, required): Match ID.
  • Response (200 OK): {"match": { ... }}

---

3.6 Organization &amp; Cohort Management (§3, §40)

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/org/cohorts</code>
  • Purpose: Academy or enterprise manager creates a learner cohort for a course.
  • Authentication: Required + Org Role (requireOrgRole("manager", "owner")).
  • Request Body:

``json { "orgId": "org_academy_01", "courseId": "crs_01HJ...", "name": "2026 Fall LLM Engineering Cohort" } ``

  • Response (201 Created): {"cohort": { ... }}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/org/cohorts?orgId=...</code>
  • Purpose: Lists all cohorts within an organization.
  • Authentication: Required + Org Role (requireOrgRole("member", "manager", "owner")).
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/org/cohorts/[id]/members</code>
  • Purpose: Adds a student or employee to a cohort.
  • Authentication: Required + Org Role (requireOrgRole("manager", "owner")).
  • Request Body:

``json { "userId": "usr_01HJ...", "email": "student@university.edu", "role": "student" } ``

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/org/cohorts/[id]/members</code>
  • Purpose: Lists cohort roster and individual mastery progress.
  • Authentication: Required + Org Role (requireOrgRole("member", "manager", "owner")).
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">DELETE /api/org/cohorts/[id]/members</code>
  • Purpose: Removes a member from a cohort.
  • Authentication: Required + Org Role (requireOrgRole("manager", "owner")).
  • Request Body: {"userId": "usr_01HJ..."}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/org/workforce-report?orgId=...</code>
  • Purpose: Lists generated workforce capability reports and feature flag status.
  • Authentication: Required + Org Role (requireOrgRole("member", "manager", "owner")).
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/org/workforce-report</code>
  • Purpose: Triggers generation of an enterprise workforce intelligence report (skill distribution, gap analysis, cohort velocity). Gated by feature flag workforce_reports.enabled.
  • Authentication: Required + Org Role (requireOrgRole("manager", "owner")).
  • Request Body: {"orgId": "org_enterprise_01"}
  • Response (201 Created): {"report": { ... }}

---

3.7 Learning Runtime &amp; Interactive Activities (§7, §10, §13)

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/activities/[id]/assistance</code>
  • Purpose: Requests structured tutor assistance during an activity (hint, explanation, remediation branch, or reveal solution). Enforces support-debt tracking.
  • Authentication: Required (requireUserId).
  • Path Parameters:
  • id (string, required): Activity ID.
  • Request Body:

``json { "kind": "hint", "attemptId": "att_01HJ...", "content": "I am unsure how the key-value projection dimensions align in multi-head attention." } ``

  • Allowed Kinds: "hint" | "explanation" | "remediation" | "solution".
  • Response (200 OK): {"event": { "kind": "hint", "content": "..." }}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/activities/[id]/attempts</code>
  • Purpose: Records learner's answer attempt for an activity, calculates score, updates LearnerSkillState, and evaluates prerequisite diagnostic triggers.
  • Authentication: Required (requireUserId).
  • Path Parameters:
  • id (string, required): Activity ID.
  • Request Body:

``json { "response": { "selectedOptionIndex": 2 }, "correct": true, "score": 1.0 } ``

  • Response (200 OK):

``json { "attempt": { "id": "att_01HJ...", "correct": true, "score": 1.0 }, "evidenceResult": { "skillNodeId": "sk_node_ml_01", "level": "L3_proficient", "mastered": true }, "diagnosis": null } ``

---

3.8 Compiler &amp; Source Ingestion (§6, §8)

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/sources</code>
  • Purpose: Ingests raw learning sources (PDF, EPUB, DOCX, Markdown, URL, or audio transcript). Extracts text, creates source spans with provenance tracking, and persists to configured storage.
  • Authentication: Required (requireUserId).
  • Content-Type: multipart/form-data.
  • Form Fields:
  • file (File, required): Binary source file (max 50MB).
  • sourceType (string, required): "pdf" | "epub" | "docx" | "md" | "url" | "audio_transcript".
  • Response (201 Created):

``json { "document": { "id": "src_doc_01HJ...", "title": "Attention Is All You Need.pdf", "sourceType": "pdf", "status": "ready", "chunkCount": 24 } } ``

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/sources</code>
  • Purpose: Lists source documents uploaded by the authenticated user.
  • Authentication: Required (requireUserId).
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/compile</code>
  • Purpose: Dispatches progressive course compilation pipeline for an ingested document. Uses Inngest Cloud workflow or local dev runner.
  • Authentication: Required (requireUserId).
  • Request Body: {"documentId": "src_doc_01HJ..."}
  • Response (200 OK): {"job": { "id": "cpj_01HJ...", "status": "pending" }}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/compile</code>
  • Purpose: Lists compilation jobs for the user.
  • Authentication: Required (requireUserId).

---

3.9 Billing &amp; Subscriptions (§33, §44)

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET /api/billing/plans</code>
  • Purpose: Public catalog of active subscriptions and credit packs.
  • Authentication: None (Public).
  • Response (200 OK):

``json { "plans": [ { "key": "premium_monthly", "kind": "subscription", "displayNameKey": "billing.plans.premium_monthly", "priceMinor": 33000, "currency": "TWD", "monthlyPoints": 2000, "pointsGrant": null, "isActive": true }, { "key": "credits_pack_1000", "kind": "credit_pack", "displayNameKey": "billing.plans.credits_pack_1000", "priceMinor": 100000, "currency": "TWD", "monthlyPoints": null, "pointsGrant": 1000, "isActive": true } ] } ``

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/billing/checkout</code>
  • Purpose: Initiates checkout for a subscription or credit pack. Routes currency automatically (TWD → NewebPay MPG, non-TWD → PayPal Orders).
  • Authentication: Required (currentUserId).
  • Request Body:

``json { "productKey": "premium_monthly", "successUrl": "https://example.com/billing?success=true", "cancelUrl": "https://example.com/billing?canceled=true" } ``

  • Response (200 OK):
  • For NewebPay: Form post parameters (MerchantID, TradeInfo, TradeSha, Version, actionUrl).
  • For PayPal: { "checkoutUrl": "https://www.sandbox.paypal.com/checkoutnow?token=...", "orderId": "..." }.

---

3.10 Webhooks &amp; Inbound Gateways

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/billing/webhook/newebpay</code>
  • Purpose: Receives asynchronous MPG payment notification from NewebPay.
  • Security: Verifies TradeSha SHA-256 checksum with NEWEBPAY_HASH_KEY and NEWEBPAY_HASH_IV. Decrypts AES-256-CBC TradeInfo. Idempotent processing via webhook_receipt.
  • Response (200 OK): {"status": "SUCCESS", "duplicate": false}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/billing/webhook/paypal</code>
  • Purpose: Receives asynchronous order capture notifications from PayPal.
  • Security: Inspects transmission headers (paypal-transmission-id, paypal-transmission-sig), verifies certificate URL belongs to *.paypal.com, and computes HMAC when PAYPAL_WEBHOOK_ID is set.
  • Response (200 OK): {"status": "SUCCESS", "duplicate": false}
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/internal/student-verification/inbound</code>
  • Purpose: Internal endpoint invoked by Cloudflare Email Worker when a student email is received at verify+TOKEN@....
  • Security: HMAC-SHA256 signature verification over the raw body via header x-signature-sha256 or x-signature using STUDENT_VERIFY_HMAC_SECRET.
  • Payload: { "recipient": "verify+TOKEN@...", "envelopeSender": "student@ntu.edu.tw", "authResults": { "spf": "pass", "dkim": "pass", "dmarc": "pass" } }.
  • Response (200 OK): {"success": true, "status": "verified", "level": "educational_email_verified"}

---

3.11 Admin &amp; Operational Endpoints

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">POST /api/admin/edu-domains</code>
  • Purpose: Administrator manual upsert into the global educational_domain registry.
  • Authentication: Required + Admin Role (requireRole("admin")).
  • Request Body:

``json { "domain": "ntu.edu.tw", "institutionName": "National Taiwan University", "countryCode": "TW", "institutionType": "university", "status": "trusted", "studentOnly": false } ``

<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET|POST|PUT /api/inngest</code>
  • Purpose: Inngest Cloud workflow webhook handler dispatching background compilation jobs (compileJobFunction).
  • Authentication: Managed by Inngest SDK (INNGEST_SIGNING_KEY / INNGEST_EVENT_KEY).
<code class="rounded bg-black/25 px-1.5 py-0.5 font-mono text-[13px]">GET|POST /api/auth/[...all]</code>
  • Purpose: Better-Auth core handler for password sessions, OAuth callbacks, account recovery, and two-factor TOTP verification.
  • Security: Account recovery uses /api/auth/recovery/request with multi-signal risk assessment; no magic-link endpoint is supported. 2FA uses twoFactor.enable, twoFactor.verifyTotp, twoFactor.verifyBackupCode, and twoFactor.disable.