Appearance
Publishing to the Store
The Typora extension store at extensions.typora.io is built from one public repository, github.com/typora/typora-extensions. Extensions are never uploaded as binaries:
- Every extension lives in the repository as source.
- Every new extension and every update is a pull request.
- Typora's CI validates, packages and publishes exactly what was reviewed.
Users find and install store extensions from Settings ▸ Extensions, which reads the store's catalog.
Submitting an extension
Fork typora/typora-extensions and add your extension as
extensions/<your.id>/. The folder name must equal the manifestid(for examplejane.plantuml). The id is permanent.Fill in
name,version, a one-sentencedescription,author,tags(the store's filters:Language Pack,Markdown Extension,Diagram,Math,Theme,Writing,Template,AI,Media,Image Uploader,Export,Publish,Data Sync,Productivity,File Viewer,View,Developer) and optionallykeywords(store search terms). AREADME.mdin the folder is shown on the extension's store page. Two optional images show Typora with the extension at work:cover.webp, 800 × 400 (2:1), on the store card — crop it from the screenshot without the window title bar — andscreenshot.webp, 1760 × 1100 (16:10: an 880 × 550 window at 2x, no scrollbars), on the extension's page. Use WebP (lossless for screenshots, so text stays sharp) and keep each under 300 KB; PNG and JPEG also work, up to 1 MB. They are not packaged, and changing only them needs no version bump.Validate locally (Node 22, no dependencies):
shnpm run validate # every extension, or: node scripts/validate.mjs jane.plantuml npm run build # build the catalog and zips into dist/Open a pull request. Explain each permission you ask for in the description.
After review and merge, the deploy workflow publishes the new version within a few minutes.
Every change to an extension's folder needs a version bump. Published packages are immutable and cached forever; CI fails a pull request that changes files without bumping version.
Unlisted extensions
For an extension still in development, add these top-level fields to typora-extension.json (the example is for a sync provider):
json
"tags": ["Data Sync"],
"unlisted": trueunlisted is an optional boolean. When it is true:
- The extension has no homepage card and does not appear in website search, tag-filter results, or the app's extension browsing/search results.
- Its page at
https://extensions.typora.io/<id>/remains accessible. Share that URL so testers can use Install or download the package. - The page is omitted from the sitemap and includes a
noindexrobots meta tag to ask search engines not to index it. index.jsonretains the entry withunlisted: trueso direct install links can resolve its package and checksum.
This controls discovery, not access: the source, catalog entry, page, and package remain public. It does not disable an already installed extension or bypass permission prompts. Search engines may take time to remove a previously indexed page.
Omit unlisted or set it to false when ready for public discovery. Changes to this manifest field follow the same version-bump and publishing process as other manifest changes. The Data Sync tag is independent of visibility; use it for sync providers whether listed or unlisted.
Automated checks
Every pull request runs:
- Manifest checks: required fields, id pattern and folder name, permission shapes, and that every referenced file (
main,background,entrypages, template and snippet folders) exists. - File checks: no symbolic links, at most 1 MB per file and 5 MB per extension. Dotfiles are not packaged. Language packs and MathJax fonts may be larger: 32 MB per dictionary file, 8 MB per font file, 128 MB per extension; font folders are not flagged as minified.
- Code checks: flags
eval(,new Function(, remoteimport()/importScripts()/<script src>, and very long (minified) lines. - Permission diff against
main: permissions added or removed, network origins, tier change, and the review depth below, printed in the job summary.
Review depth follows permissions
| The extension | Review |
|---|---|
L0 (data only), no network | Automated checks plus a light review |
Declares network, document, workspace or clipboard | Human review of the code paths that use them |
| An update that widens permissions | Human review; users are asked to consent again on update |
Rules
- No remote code. Everything that runs ships in the package. Don't load scripts, styles or WebAssembly from the network, and don't
evalanything fetched or read from a document. - No minified or obfuscated sources. Commit readable source. If you use a build step, commit its output in readable form together with the source, and say so in the pull request.
- Ask for the least. List network origins exactly (
https://kroki.io, nothttps://*). - Widening permissions is an explicit change. Adding an origin, going from
document: readtowrite, or addingworkspace/clipboardmust be called out in the pull request. - Documents are data. Treat fence bodies, front matter and file contents as untrusted input.
- Keep the package small: no
node_modules, build caches or test fixtures users don't need.
Reviewers may decline extensions that duplicate built-in features, collect data without saying so, or are unmaintained. A published version that turns out to be harmful is revoked and disabled in the app.
Testing against a local store
npm run dev in the repository builds the catalog for http://localhost:8788 and serves it. Start Typora with the environment variable TYPORA_EXTENSIONS_STORE=http://localhost:8788 to browse and install from it.
What gets published
For each extension, CI produces a deterministic package — a zip named download/<id>-<version>.typora-extension (the same source always gives the same SHA-256) and an entry in the catalog (index.json) with the extension's metadata, permissions, tier (L0, L1 or L2) and the download's hash. Typora checks the hash before installing.