{
  "openapi": "3.1.0",
  "info": {
    "title": "GlyphGuard",
    "version": "0.1.0",
    "description": "Hidden and deceptive Unicode detection (invisible characters, bidi controls, tag payloads, mixed scripts, lookalikes), text cleaning and lookalike name comparison. Pure computation; nothing stored; no API key. About 60 checks per minute per IP. Risk: high = bidi overrides/isolates, Unicode tag characters, mixed-script words or lookalikes inside them; medium = zero-width and other invisible characters, control characters, lookalike letters in Latin-looking words; low = variation selectors, unusual spaces, private-use characters; lone surrogates (malformed UTF-16) are medium. Joiners in legitimate context are info (type joiner_in_context, risk unchanged): ZWJ inside emoji ZWJ sequences, VS16 emoji presentation, ZWNJ/ZWJ between letters of joining scripts (Arabic, Persian) and after an Indic virama. ZWJ/ZWNJ anywhere else (for example inside an ASCII word) stay medium. clean keeps the in-context joiners by default (keep_joiners=true); strict=true removes them all. Offsets are UTF-16 code units (JavaScript string indexes). Up to 100 findings are listed; counts are always complete. Legitimate text in Cyrillic, Greek or CJK on its own is not flagged; only words that mix scripts are. GlyphGuard is not affiliated with, endorsed by or sponsored by the Unicode Consortium, Meta, Anthropic, OpenAI or Anysphere. Results are a screening signal, not a security guarantee.",
    "contact": {
      "email": "digitalpromohub.support@gmail.com"
    }
  },
  "servers": [
    {
      "url": "https://glyphguard.mike-tusa.workers.dev"
    }
  ],
  "paths": {
    "/api/scan": {
      "get": {
        "operationId": "scanText",
        "summary": "Scan text for hidden or deceptive Unicode",
        "parameters": [
          {
            "name": "text",
            "in": "query",
            "required": true,
            "description": "Text to inspect, URL-encoded (max 2000 UTF-16 code units).",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2000
            },
            "example": "pаypal​"
          }
        ],
        "responses": {
          "200": {
            "description": "Scan result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Scan"
                }
              }
            }
          },
          "400": {
            "description": "invalid_input (missing/oversized input is rejected before the rate limiter)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "scanTextPostNotAllowed",
        "summary": "Not supported (use GET)",
        "responses": {
          "405": {
            "description": "method_not_allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/clean": {
      "get": {
        "operationId": "cleanText",
        "summary": "Remove hidden Unicode from text",
        "parameters": [
          {
            "name": "text",
            "in": "query",
            "required": true,
            "description": "Text to inspect, URL-encoded (max 2000 UTF-16 code units).",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2000
            },
            "example": "pаypal​"
          },
          {
            "name": "lookalikes",
            "in": "query",
            "required": false,
            "description": "Map lookalike letters to Latin.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "nfkc",
            "in": "query",
            "required": false,
            "description": "Apply NFKC first.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "spaces",
            "in": "query",
            "required": false,
            "description": "Turn unusual spaces into normal spaces.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "keep_joiners",
            "in": "query",
            "required": false,
            "description": "Keep ZWJ/ZWNJ/VS16 in legitimate context (emoji ZWJ sequences and VS16 presentation, Arabic/Persian joining, Indic virama). Joiners elsewhere are always removed.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "strict",
            "in": "query",
            "required": false,
            "description": "Remove every joiner and variation selector, even in emoji and Persian/Indic text (overrides keep_joiners).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cleaned text",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Clean"
                }
              }
            }
          },
          "400": {
            "description": "invalid_input (missing/oversized input is rejected before the rate limiter)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "cleanTextPostNotAllowed",
        "summary": "Not supported (use GET)",
        "responses": {
          "405": {
            "description": "method_not_allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/compare": {
      "get": {
        "operationId": "compareNames",
        "summary": "Is a name a lookalike of protected names?",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128
            },
            "example": "pаypal"
          },
          {
            "name": "targets",
            "in": "query",
            "required": false,
            "description": "1-50 protected names separated by | (alternatively repeat target=).",
            "schema": {
              "type": "string",
              "maxLength": 6450
            },
            "example": "paypal|apple"
          }
        ],
        "responses": {
          "200": {
            "description": "Comparison",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Compare"
                }
              }
            }
          },
          "400": {
            "description": "invalid_input (missing/oversized input is rejected before the rate limiter)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "compareNamesPostNotAllowed",
        "summary": "Not supported (use GET)",
        "responses": {
          "405": {
            "description": "method_not_allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "health",
        "summary": "Health and version",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "version": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok",
                    "version"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/mcp/server-card.json": {
      "get": {
        "operationId": "serverCard",
        "summary": "Static MCP server card (same tools as tools/list)",
        "responses": {
          "200": {
            "description": "Server card",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp",
        "summary": "Remote MCP server (JSON-RPC 2.0 over Streamable HTTP, stateless)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "JSON-RPC 2.0 request, notification or batch (max 10, at most one tools/call)"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "JSON-RPC 2.0 response (or batch array)"
                }
              }
            }
          },
          "202": {
            "description": "Notification or client response accepted (empty body)"
          },
          "400": {
            "description": "Parse error, invalid request or protocol/header mismatch",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "JSON-RPC 2.0 response (or batch array)"
                }
              }
            }
          },
          "403": {
            "description": "Origin not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "JSON-RPC 2.0 response (or batch array)"
                }
              }
            }
          },
          "404": {
            "description": "Method not found (2026-07-28 requests)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "JSON-RPC 2.0 response (or batch array)"
                }
              }
            }
          },
          "413": {
            "description": "Body too large",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "JSON-RPC 2.0 response (or batch array)"
                }
              }
            }
          },
          "429": {
            "description": "Too many MCP requests (Retry-After)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "JSON-RPC 2.0 response (or batch array)"
                }
              }
            }
          },
          "500": {
            "description": "Internal error"
          },
          "503": {
            "description": "Temporarily unavailable"
          }
        }
      },
      "get": {
        "operationId": "mcpGet",
        "summary": "Not supported (no SSE stream)",
        "responses": {
          "405": {
            "description": "Method not allowed",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "POST, OPTIONS"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "mcpDelete",
        "summary": "Not supported (no sessions)",
        "responses": {
          "405": {
            "description": "Method not allowed",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "POST, OPTIONS"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_input",
                  "rate_limited",
                  "method_not_allowed",
                  "not_found",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              },
              "field": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "Scan": {
        "type": "object",
        "properties": {
          "risk": {
            "type": "string",
            "enum": [
              "none",
              "low",
              "medium",
              "high"
            ]
          },
          "length": {
            "type": "integer"
          },
          "codePoints": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "counts": {
            "type": "object"
          },
          "tagPayload": {
            "type": "string"
          },
          "findings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string"
                },
                "severity": {
                  "type": "string"
                },
                "offset": {
                  "type": "integer"
                },
                "codePoint": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "char": {
                  "type": "string"
                },
                "looksLike": {
                  "type": "string"
                },
                "script": {
                  "type": "string"
                },
                "word": {
                  "type": "string"
                },
                "scripts": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "truncated": {
            "type": "boolean"
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "normalizedNfkcDiffers": {
            "type": "boolean"
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "risk",
          "total",
          "counts",
          "findings",
          "version"
        ]
      },
      "Clean": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          },
          "changed": {
            "type": "boolean"
          },
          "removed": {
            "type": "object"
          },
          "joinersKept": {
            "type": "integer"
          },
          "lookalikesMapped": {
            "type": "integer"
          },
          "options": {
            "type": "object"
          },
          "length": {
            "type": "integer"
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "text",
          "changed",
          "removed",
          "version"
        ]
      },
      "Compare": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "skeleton": {
            "type": "string"
          },
          "confusableWith": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "target": {
                  "type": "string"
                },
                "skeleton": {
                  "type": "string"
                },
                "identical": {
                  "type": "boolean"
                },
                "confusable": {
                  "type": "boolean"
                },
                "caseOnly": {
                  "type": "boolean"
                }
              }
            }
          },
          "nameRisk": {
            "type": "string"
          },
          "nameScripts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "skeleton",
          "confusableWith",
          "results",
          "version"
        ]
      }
    }
  }
}