{
  "name": "reforger-workshop-api",
  "description": "Cached proxy over the Arma Reforger Workshop for generating server-config mods arrays. Data is cached for 30 minutes.",
  "endpoints": {
    "GET /": "Debug UI: browse the workshop, inspect mods, and build config mods arrays.",
    "GET /api": "This usage document.",
    "GET /api/mods?search=&page=&sort=": "Search/list mods. Search matches names and mod-id substrings. 16 results per page.",
    "GET /api/mods/{modId}": "Full mod detail: versions, dependencies, screenshots, logo, and a ready configEntry.",
    "GET /api/mods/{modId}/config?pin=false&deps=true": "Config-ready mods array for one mod, dependencies resolved transitively. pin=true adds version fields. Addons the server could not download (blocked, private, unpublished or deleted) are left out of the array and named in a warning; the ids are also listed in \"blocked\". 410 when the requested mod itself is blocked or removed, 404 when it never existed.",
    "POST /api/config": "Body {\"ids\": [\"...\"], \"pin\": false, \"deps\": true}. Merged, deduplicated mods array for many mods. Blocked/private/unpublished/deleted addons are omitted from \"mods\", named in \"warnings\" and listed in \"blocked\". Responses may include a non-empty \"unresolved\" array of mod ids when the per-request fetch budget is hit; POST those ids again to continue where the previous request stopped (the debug UI does this automatically).",
    "POST /api/validate": "Body {\"ids\": [\"...\"]} or {\"mods\": [{\"modId\": \"...\"}]} (a server-config mods array pasted as-is). Checks a mod list for addons that would stop a dedicated server from starting — the addon itself is undownloadable (blocked or private), or something in its dependency tree is (blocked, private, unpublished or deleted). Unlisted addons still download by id, so they appear in \"notes\" and never affect the outcome. Returns a per-mod status (ok / blocked / tree-blocked / not-found / invalid-id), a \"verdict\" of will-start / will-not-start / incomplete, and \"complete\": true only when nothing is left in \"unchecked\". \"willServerStart\" is true ONLY for a complete check with zero problems of any status (invalid ids included) — an incomplete or partly failed run is never green. Like /api/config, ids left over when the fetch budget runs out come back in \"unchecked\" (and are counted in summary.unchecked, so checked + unchecked covers every submitted id); POST those again to continue. EXTENDED MODE (additive, all optional): send a \"version\" on a mods[] entry, and/or \"scenarioId\", \"platforms\": [\"PLATFORM_PC\",…] and \"gameVersion\" — the same one-fetch-per-mod loop then also returns per-result \"pin\" (ok / not-found / invalid-format / not-checked; not-found and invalid-format are problems), a \"scenario\" block (vanilla / found / not-found / retired / not-checked — \"not-found\" only ever on a complete run) with the \"providerModId\" that publishes it, a \"packSize\" block (deduplicated dependency-tree bytes, per-mod breakdown, and a console budget check when XBL/PSN are in \"platforms\"), and a \"diagnostics\" array using live.* rule ids. Continuation merge rules: a scenario \"found\" in any round wins (drop scenarioId from later rounds once found), pack-size totals are summed by the client across rounds (each round's perMod is disjoint), and packSize.complete is false while anything is unchecked. A body without any of those inputs behaves byte-identically to before.",
    "GET /api/catalog": "Everything needed to build a config UI without embedding these rules: latestGameVersion, gameVersions[], the 33-entry vanilla scenario catalog (31 live + 2 retired with replacedBy, each with a headerClass), the missionHeader key tables per header class (+ legacy/nonexistent keys and the named enums), the config-level legacy-key migration map, official-only/internal/fabricated key lists, per-field limits derived from the ground-truth 1.8.0.10 DSConfigSchema, documented defaults, console pack-size budgets, and the rule table (id → default severity + description). In \"limits\", an integer field carries BOTH \"type\": \"integer\" and \"integer\": true — the two are aliases, kept so either spelling works. Cacheable for 30 minutes.",
    "POST /api/config/validate": "Body {\"config\": \"<raw file text>\" | {…}, \"gameVersion\": \"1.8.0.10\", \"readOnlyPaths\": [\"/bindPort\"], \"skipRules\": [\"xfield.fast-validation-off\"]}. Validates a whole server config: JSON sanitizing (BOM, smart quotes, comments, trailing commas, single quotes, duplicate keys), legacy-key migration, key-case repair, the ground-truth JSON Schema, the strict lint layer, ~40 cross-field rules and version gating. Prefer sending the raw TEXT: only the string form can diagnose and repair broken JSON. Returns \"diagnostics\" (stable \"rule\" id + RFC 6901 \"path\" + severity + optional \"fix\" ops), \"summary\", \"schemaValid\", \"verdict\"/\"willServerStart\" (green only when errors are zero AND nothing needs a live check), and \"liveCheck\" — a ready-to-POST body for /api/validate. IMPORTANT: any \"json.*\" warning (BOM, comment, smart/single quotes, trailing or missing comma, bad escape, raw control character, duplicate key) means the SUBMITTED bytes are not valid JSON — the verdict describes the repaired document, so upload the \"configText\" from /api/config/fix rather than the original file. Makes zero upstream requests.",
    "POST /api/config/fix": "Same body as /api/config/validate. Applies every fix the validator emitted (except those under \"readOnlyPaths\" or from \"skipRules\"), re-validates, and returns the validate response plus \"fixed\", \"config\" (repaired object), \"configText\" (2-space, canonical key order — the exact bytes to upload) and \"changes\" (rule, path, op, from, to, message). Note the difference in what the two options guard: \"readOnlyPaths\" protects a VALUE (nothing under those paths is set, removed or renamed away — by any rule), while \"skipRules\" only mutes a RULE (another rule's fix may still touch the same key; e.g. skipping key.fabricated does not stop schema.additional-prop removing \"playerBanList\" — use readOnlyPaths to preserve it). Repairs the reference does not license — the wiki template's maxPlayers/port 0 placeholders, an RCON password, a partial platform set — are deliberately NOT invented and stay as errors.",
    "POST /api/config/generate": "NOT the same as POST /api/config (which only builds a mods array). Body {\"name\", \"scenarioId\"} required, plus optional maxPlayers, password, passwordAdmin, admins[], crossPlatform, bindAddress, publicAddress, ports{game,a2s,rcon}, rcon{password,permission}, mods[], gameProperties, missionHeader, persistence, operating, gameVersion. Emits a complete config.json (explicit a2s block, explicit rcon.permission, fastValidation true, crossPlatform alone, boolean disableThirdPerson on pre-1.8 targets), self-checks it through the validator and returns \"config\" + \"configText\" with the same diagnostics/verdict fields. Dependencies are NOT resolved here — the wizard flow is POST /api/config (dependencies) → POST /api/config/generate → POST /api/validate (live checks)."
  },
  "diagnostics": {
    "ruleNamespaces": {
      "json.*": "file-level syntax and sanitizing",
      "key.*": "unknown, miscapitalized, official-only, internal or fabricated keys",
      "legacy.*": "renamed/removed keys and type migrations",
      "schema.*": "the server's own JSON Schema verdict",
      "lint.*": "formats stricter than the schema floor",
      "xfield.*": "cross-field rules (a server that boots but does not work)",
      "version.*": "target-version gating",
      "live.*": "workshop checks from POST /api/validate"
    },
    "severities": "error | warning | info — defaults per rule id are served by GET /api/catalog.",
    "composition": "Final green = /api/config/validate errors == 0 AND /api/validate willServerStart === true for the returned liveCheck body (merged across continuation rounds)."
  },
  "source": "Data from reforger.armaplatform.com (Bohemia Interactive). Unofficial, cached 30 min."
}