Unofficial. Not affiliated with, endorsed by, or connected to OpenFront or its developers.
Chrome/Edge extension (Manifest V3) that shows each player's world ranking next to their name in OpenFront — in the lobby list, on the hover panel during a game and on the in-game leaderboard.
[LUX] TeNa Top 3% 🔥 Firedan Top 1% 🗺0.5% [DFY] Rage Top 2%
![]() |
![]() |
![]() |
![]() |
![]() Stream overlay |
![]() Clan hub |
![]() Tournaments |
Lobby players, the game map behind the overlay and the timelapse frames are made up; dashboard, clan and tournament data come from public records.
chrome://extensions→ enable Developer mode.- Load unpacked → select this folder.
- On the first-run page that opens, press "I agree" (see below).
- Open https://openfront.io and enter a lobby.
It needs Chrome (or a Chromium browser) 114 or newer (minimum_chrome_version):
from 114 on chrome.storage.local holds 10 MB, room for the tournament cache
and the stream overlay's recap card and replay together.
Click the toolbar icon for the settings: theme and website layout, which badge marks and scouting features are on, the recap and timelapse, streamer mode, the optional chat, your watchlist, a status readout and "Clear cached ranks". The switch at its top turns everything off.
On install a page opens that says, in plain words, what leaves the browser:
player names from your lobby and game (yours included), clan tags and names you
look up in the dashboard go to api.ofstats.io, a third-party community
statistics service, and a finished game's id (or one you check in the Observer
page) goes to OpenFront's public API.
Nothing is looked up until you press "I agree"; the worker refuses lookup
messages until then (dataConsent), and the popup can switch it off again
("Turn rank lookups off"). Themes and layouts work without it; ranks, the home
card, the dashboard and the recap stay off. Closing the page without
answering counts as "not now", through every later update too; only a profile
updated from a version before 5.7.0 (which had no first-run page and always
looked names up) keeps lookups on. Chat has its own, separate agreement. See
PRIVACY.md.
With the popup's top switch (Everything on / off) off, nothing is looked up or sent on its own: no badges, no home card, no recap records, no map preview, no chat.
Permissions are the minimum: storage, scripting (to inject the packaged
scripts into every openfront.io page as it loads, which also covers tabs
already open at install / update and site access set to "on click"; see
Getting it to run), notifications (watchlist alert,
only with Sound and notification on), and host access to openfront.io and
api.ofstats.io. clipboardWrite is optional and only requested if you tick
"Copy the report at 10 s". The extension reads the game's pages and never
sends a game action.
ofstats.io aggregates every public OpenFront game by
account: a player is their public OpenFront player id
(api.ofstats.io/players/<id>), and a name answers 404. The lobby, though, only
gives names: [TAG] name (one space) for a player with a clan tag, the bare
name otherwise - OpenFront sends each lobby client's username, clan tag and a
per-game client id, never the account id. So the extension finds the account:
- Exactly, where it can: your own id is read from OpenFront's page (the account answer it fetches for guests and signed-in players alike), every game record the recap reads lists each player's id, and a clan's member list on ofstats carries its members' ids. These are remembered (up to 5000 names).
- By matching the name otherwise, with ofstats' name index
(
/names/<name>: the newest games played under that name, each with the account and the exact[TAG] nameit played as). The account that played under exactly this name most recently wins (same case first); then the index's account list, then ofstats' search. A shown tag has to match - the bareTeNais not[LUX] TeNa, and is often somebody else. - Public free-for-alls hide clan tags (OpenFront sets
disableClanTagson every one), so[TSI] Harry Tohssits in that lobby as plainHarry Tohs. The page probe reports it, and there a bare name matches the name played under any tag.
Checked against the ids in the records of three public games (two
free-for-alls, one team game; 167 named players, named as their lobbies showed
them, each game left out of the index it was matched in, as a lobby's game is):
140 matched right, 6 wrong, 1 not found, 20 shared (below) - without the
any-tag rule for hidden tags: 127 right, 12 wrong, 8 not found. Where other
accounts play under the same name and the pick is not clear-cut (under 60% of
the recent games under it), the badge says so with ≈: all 122 clear picks were
right, 18 of the 24 marked ones. A match is kept 6 hours, so a name costs the
index request only the first time.
GET https://api.ofstats.io/players/<id> returns per-map rows with wins and
expectedWins — and expectedWins already accounts for how many players were in
each lobby. So:
skill = (wins + K) / (expectedWins + K) K = 3
is "how many times more often this player wins than an average player would in the same lobbies", on one scale for a 60-player free-for-all and a 1v1 alike. K shrinks the figure toward parity so a 4-game hot streak cannot outrank a 1000-game veteran.
That ratio is converted to a percentile through a table calibrated against real
lobby data (tools/calibrate.mjs), and shown as Top 23%.
Why not ofstats' own winsTop? It only has the buckets 0.01 / 0.1 / 1 / 5,
so most of a lobby came out as "Top 1%". Measured on 94 rated players sampled
from live public games, the current table gives 58 distinct labels, with the
most common one on 7 of 94 players, spread across bands:
| Band | Percentile | Colour | Share of sample |
|---|---|---|---|
| elite | Top 5% | red | 3 |
| strong | Top 6–15% | amber | 8 |
| good | Top 16–35% | green | 12 |
| average | Top 36–60% | blue | 30 |
| low | below | grey | 41 |
Players with fewer than 3 rated games fall back to 12.5% WR · 48. The call is
made from the service worker, because ofstats sends no
Access-Control-Allow-Origin and a page-context fetch is blocked.
Rather than leaving a blank, the extension marks the reason (turn this off in the popup):
| Badge | Meaning |
|---|---|
hidden |
OpenFront's Hidden Names setting is on, so every other player's name is replaced on your screen with a tribe name. Nothing real is left to look up — this is the usual reason for "I only see myself". Turn it off in OpenFront settings. |
guest |
A generated Anon… handle, used by players who never set a name. Thousands of people share each one, so no rank can belong to it. |
shared |
An untagged name that 1000+ OpenFront accounts have played under - usually one the game suggests ("Peace And Love", "NAPOLEON"). Matching it to an account was right about a third of the time, so no rank is guessed. |
new |
No finished public games on ofstats yet. |
≈ Top 12% |
Not missing, but worth a look: the name was matched to an account while other accounts also played under it lately, and the pick is not clear-cut. The tooltip says how many. |
offline |
ofstats could not be reached, or answered with an error. A failure is cached for only a minute, so that name is asked again soon (a real "no history" answer is kept for 30 minutes). |
- Lobby list — free-for-all tags, team cards, and the unassigned column.
- In-game hover panel — the card that follows your cursor over a territory.
- In-game leaderboard — the player table in the left sidebar.
That panel and table also cover bots and nations, whose generated names can collide with a real account's (a nation called "France", a player called "France"), and nothing in their markup reliably says which is which in every language and mode. So the extension remembers the lobby roster and only badges names that were in it. Enable it mid-game with no lobby seen and there is no roster to check against, so it falls back to badging any name ofstats knows.
Above the player list:
avg Top 39% · 2 elite · 2 unranked · [LUX] x2 Top 3%
— the room's average percentile, how many are elite (Top 5%), how many could not
be ranked, and any clan with more than one player present, strongest first. Each
team card gets its own team avg Top N%, so you can see before the game starts
whether your side is outmatched.
The strongest one or two players in the room (elite or strong only) get their row outlined, in every place they appear. In a weak lobby nobody lights up — being the least bad is not a threat.
The lobby names the map but never shows it, so the preview puts a thumbnail and
World · Free For All · Easy · 400 tribes above the player list.
Thumbnails are content-hashed (thumbnail.2110da80df3c.webp), so their URLs
cannot be guessed. The mapping lives in window.BOOTSTRAP_CONFIG.assetManifest,
a page global that an isolated content script cannot read — so
src/page-probe.js runs in the MAIN world, reads the manifest plus the lobby
element's own state, and publishes {map, thumbnail, mode, difficulty, bots} on
a data-ofr-map attribute that the content script watches.
Two shapes exist in the live build: the host modal keeps selectedMap /
selectedDifficulty as flat properties, the join modal carries a gameConfig
object. The probe reads both, and polls once a second because the host can
switch maps mid-lobby with no event to hook from outside.
The map folder is the GameMapType key lowercased — New York City becomes
newyorkcity, so the name is stripped of non-alphanumerics rather than
slugified with dashes.
Click the lobby preview to open a full-screen view: scroll to zoom (anchored on
the cursor), drag to pan, + / - / Fit in the bar, Esc to close.
It does not enlarge the 500x250 thumbnail — that turns to mush. It renders the
real terrain: maps/<map>/map.bin is one byte per tile (bit 7 land, bit 6
shoreline, bit 5 ocean, low 5 bits elevation) and the map's manifest.json
gives the dimensions (2000x1000 for World, one byte each, so the file is exactly
2,000,000 bytes). The colours mirror the game's own encodeTerrainTile, so the
view matches what you will play on, and drawing uses nearest-neighbour when
magnified for the same pixel-crisp look the game uses.
The CDN sends permissive CORS headers, so the fetch works from the content script. If terrain cannot be loaded the view falls back to the thumbnail and says so.
All of these come from the same single ofstats request per player (after the name has been matched to an account, see above) — no extra network traffic.
| Badge mark | Meaning |
|---|---|
🗺3% |
Skill on the map being played, from ofstats' per-map wins vs expected (needs 5+ games on it). |
🔥 / ❄ |
Form. Hot: 3+ win streak, or last-ten wins at least double their norm. Cold: no wins in ten for someone who usually gets one. |
⚠ |
Possible smurf: 5–40 rated games with a raw (unshrunk) ratio of 2.5x+. The percentile shrinks small samples on purpose, which is exactly what hides a strong player on a fresh account. |
★ |
On your watchlist. |
The tooltip adds a trend sparkline — win rate in blocks of six games, oldest to newest, drawn with block glyphs since native tooltips cannot render markup.
Watchlist. Shift+click any badge to star or unstar a player. Starred names are gold everywhere and a toast appears when one is in your lobby. With Sound and notification on, a sound plays too, and a system notification fires if the tab is in the background; both follow that one switch. Manage the list in the popup.
Copy. The summary line has a Copy button that puts a Discord-ready scouting report on the clipboard: map, mode, summary, then every player strongest first with their numbers and flags.
Post-game recap. See Game recap below.
The roster is always the lobby currently on screen; it is kept after the game starts (when the list disappears) because the in-game bot filter needs it.
One theme for everything. The picker at the top of the popup restyles all the extension draws - badges, lobby summaries, home card, dashboard, recap, charts, the share image, and the popup / settings panel itself - and, with Recolour OpenFront too ticked (the default), OpenFront's own pages in the same palette. Untick it to keep the game's colours and theme only the extension. The popup preview uses the same stylesheet and icon sets as the page, so what you see there is what the lobby gets.
| Theme | Look |
|---|---|
| Classic | OpenFront's own colours, the default dark UI and emoji marks |
| Neon | black glass, glowing cyan and magenta |
| Tactical | olive console, monospace, uppercase, square corners |
| Pastel | light: soft lilac panels, filled pills |
| Mono | greyscale; rank reads from brightness alone |
| High contrast | pure black, colour-blind-safe blue/orange, thick borders, words instead of pictograms (HOT, SMURF?, WIN) |
| Midnight | deep navy with a calm cyan accent; Neon without the glow |
| Ember | warm charcoal, orange and amber, rose for the top band |
| Daylight | light: crisp white and neutral grey, one indigo accent |
A theme is one id in three places, and src/themes.js is the catalogue:
src/themes.js- name, one-line description, icon set (icons can't be expressed in CSS) andsite, the recipe for OpenFront's palette: a hue per role (primary / success / danger / warning / accent), how tinted the surfaces are, a chroma boost, light or dark.src/content.css- the token blockhtml[data-ofr-theme="<id>"]. Every colour, radius and font the extension uses is a--ofr-*custom property, so a theme is only a block of overrides; nothing else carries a colour, charts and the canvas share image included (they read the tokens).src/page-themes.css- generated fromsitebynode tools/gen-page-themes.mjs. OpenFront is Tailwind v4, whose palette is CSS custom properties on:root, so overriding them on<html data-ofr-page-theme>recolours the page without touching elements. It covers every colour variable the site's stylesheets reference - 108 in 26 families, captured from the live page intotools/used-colors.json- because a hand-picked subset left:hovershades on the original palette and the theme visibly dropped under the cursor. The generator also fails if the three places disagree (a theme without a token block, or the reverse).
node tools/check-theme-contrast.mjs then checks every token block: all text
tokens and band colours at 4.5:1 on the panel, card and stage backgrounds (and
every band on its own badge fill and under --ofr-on-band), and that the five
bands and the loss colour stay apart. test/themes.html shows every theme side
by side.
Before 5.5 these were two pickers ("Theme" and "Restyle OpenFront itself",
default off). An existing profile is carried over once (marker
themesMigrated, synced with the settings) so that updating changes nothing on
screen: only the site themed -> that becomes the theme; an extension theme with
the site never themed -> Recolour OpenFront too starts unticked; both set
-> the theme picker wins and the site follows. After that a stray pageTheme
(another machine on the same profile still on 5.4) is ignored rather than
fought over.
The Copy report always uses standard emoji, since it's meant for Discord.
On the front page, right under the username field, as pictures rather than numbers: a small rank gauge, your name with the win streak as flames, today's games as dots, a win-rate bar with a tick where an average player in your lobbies would be, and the last 10 results as a strip - plus buttons to the full dashboard and the settings. Refreshes at most once a minute. It is there (and looks up your name) only while rank lookups are agreed and the extension is switched on.
A PRO button in OpenFront's nav bar opens a full-page statistics view for you (or, from the search box, any player). It is drawn, not tabulated - each section leads with a graphic, the exact figures are in hover tooltips, and a Numbers button per section (closed by default) shows the old tables:
- Overview - a world-rank gauge coloured by band, a win-rate ring with a tick for an average player in the same lobbies, the last 10 results, the streak as flames, and one icon row (games, wins, conquests, nukes, gold).
- Today's session - games won today as a ring, one bar per game (taller is a better placing), and your rank at the start of the day and now.
- Recent form and Trends - the last-60 strip, rolling win rate, survival and gold charts.
- By map - best and weakest five, then a heat grid of every map coloured by your percentile band there (fainter when there are few games).
- By mode - one win-rate ring per mode. Personal bests - icon cards.
- Recent games - a row per game with the result, map, mode and two small bars (length, conquests), linking to the game on ofstats; 10 shown, the rest on request.
- Clan leaderboard - this week - ofstats' top ten clans by points, yours highlighted, with each clan's stacked win rate as a bar.
- Compare - two rank gauges and mirrored bars per measure.
Clicking a rank badge opens the same view for that player, in the page (or nothing, if you turn it off in the popup). It never leaves the site: the player's ofstats.io page is one click further, as the "Open on ofstats.io" link in the dashboard header.
The way in from the header is the small tilted PRO tag under the OPENFRONT wordmark, next to the version number, so the logo reads "OPENFRONT PRO". If the header ever changes shape and the logo block cannot be found, it falls back to a pill after the last nav link.
Each player costs the same ofstats requests as the badges (the payload
already carries everything the player sections show); the clan section
(/clans/<TAG>), the weekly clan table (/clans) and a Compare lookup are one
request each.
Clans. The lobby summary shows a chip per clan with 2+ players present,
strongest first. Clicking a chip, or opening the dashboard for a tagged player,
adds a clan section from ofstats' /clans/<TAG>: win-rate rings (all games,
team, stacked, recent), members with the share active this month, and the top
members' wins as bars. Its Clan hub ↗ link opens that clan in the hub.
A full page of its own (src/clans.html, from the popup's Tools tab or the
dashboard's clan section; #tag=LUX opens a clan), graphics first with the
exact figures on hover:
- Leaderboard - ofstats' weekly top 50 by points: rank, the change since last week, points, stacked win rate (tick: all games), games, members who played that week, and a 12-week rank line for the clans in ofstats' timeline. Week arrows go back up to a year; the biggest climbers and fallers, and clans new to the top 50, sit above it.
- Clan - win-rate rings (all, team, stacked with ofstats' average-stack tick, recent), points per week for 12 weeks, win rate by stack size and by mode, then members: how many are active, the average member percentile, the members by rank band, and a row per member (percentile badge, win rate, games, last played) that opens into a mini profile with an ofstats.io link. Best maps are summed from the members looked up.
- Compare - two clans side by side (mirrored bars: points, rank, win rates, members, active share, average member percentile, games), both weekly point lines, and a head-to-head from games both clans were in. That comes only from what is visible - each clan's 20 latest games on ofstats (with the winner's name) and the last 60 games of each looked-up member (with their own result) - and a game whose result cannot be told is marked so. In team games a side's result rests on the members looked up: a clan split over two teams, with its winners not among them, can be miscounted.
- Recruits - strong players without a clan tag, filtered by percentile,
last game and modes played, sorted by rank, activity or games. The list
comes only from players this browser has already looked up: in your
lobbies and games, in recaps, and names typed into the dashboard's search or
Compare box. The worker keeps a small index for it in
storage.local(recruitIndex: one slim record per untagged, ranked player - name, percentile, games, wins, last game, games per mode - at most 500, the most recently looked up, none older than 60 days; "Clear cached ranks" empties it). The page reads that key only and sends nothing.
What it asks ofstats, through the worker (the consent gate, queue and cache
every lookup uses; with lookups not agreed the page shows a button to the
first-run page and sends nothing): /clans?limit=50[&week=YYYY-Www] (new
worker message clanTable, the week shown and the one before), /clans/<TAG>
per clan opened or compared, and /players/<[TAG] name> for members, six at a
time: 24 on opening a clan (all 50 of ofstats' first page on request), 12 per
clan in Compare. A member whose lookup gets no answer shows "offline", with a
Retry. In streamer mode your own name shows as "You"; the hub learns it from
openfront.io, where the extension notes it locally (selfStatsName) whenever
it reads your name. Until then streamer mode hides every name in the hub, with
a banner saying why. The hub follows the popup's Everything on / off switch.
node tools/test-clans.mjs tests weeks, movers, the recruit index and
filtering, the comparison and head-to-head maths, and the worker (the
clanTable route, the start-up purge of expired cache entries, lookups that
still answer when storage is full); node tools/shot-clans.mjs screenshots
every view through the launcher.
A page for organisers (src/tournament.html, popup -> Tools ->
Tournaments), with no server anywhere: create a tournament, paste each game's
id or link once it is played (https://openfront.io/game/<id>, #join=<id>
or the bare id), and it reads OpenFront's public record of each game and
works out the rest.
- Formats: points league / round robin (with a head-to-head grid), single-elimination bracket (seeded by the order you list, byes for the top seeds, winners advance from recorded games, any match can be set by hand), and a best-of-N series between two sides.
- Participants: players (matched by name, case-insensitive; a clan tag
only breaks ties between namesakes) or teams and clans (matched by clan tag
or by listed members). They can be picked from a game's players or clan
tags, and from its team slots where the record has them - only matchmade
team games do (OpenFront's server stamps
teamIndexthere); private lobbies, where tournaments are played, do not. Players a game has that nobody claims are listed on that game so you can assign them (or say "not in tournament"); two participants claiming one player (namesakes such as "Bob" and "bob" included) is never guessed. Hand-set bracket results and game pins name the two players, not a bracket position, so reseeding never hands them to someone else. - Scoring: points for a win, a placement table and points per player conquered; presets FFA points (10/7/5/4/3/2/1 + 1 per conquest) and Team wins only. Places count among the tournament's participants in that game, so strangers or bots in the lobby change nothing; tied places share their points. Ties in the standings: wins, then average place, then head-to-head.
- Results: a 2x PNG for Discord (standings, bracket or series, in your theme) and Copy results text.
- Saved in
chrome.storage.local, one key per tournament (tournament:<id>plus the order intournamentIdx, at most 30; the old singletournamentslist is moved over once), so two open tabs editing different tournaments never overwrite each other and the same one merges by its last-change stamp. A saved tournament that fails validation is kept untouched and listed as damaged (export or delete it), never dropped. A failed save shows a banner; when storage is full the record cache is emptied (except the open tournament's) and the save retried. Finished games' records are cached (tRec:<id>, LRU, 250 games / ~3 MB, the open tournament's never evicted), so reopening a tournament asks OpenFront for nothing. - Shared as a
.jsonfile or a share code:#t=<base64url of deflate-raw JSON>carrying the whole tournament (up to 6000 characters; bigger ones say so and point to the file). The other organiser pastes the code under Import. That is the way to share:tournament.htmlis not web-accessible, so achrome-extension://…/tournament.html#t=…link clicked on a web page (Discord in the browser included) is blocked by Chrome - it only opens when pasted into the address bar of a Chrome with the same extension id (the Share tab offers that link too, in the extension only). Under the Steam launcher only the code is ever copied (the page's address there holds the launcher's secret token). Files and codes are validated strictly (types, lengths, counts, no control or bidi characters, a cap on the unpacked size) and everything in them is shown as text only.
Game lookups are the same gameRecord message the recap uses, so they wait
for the first-run agreement (the page shows a button to it until then). They
go out two at a time, at least 400 ms apart, and back off (2 s, 4 s, 8 s) when
OpenFront answers 429; more than 20 records due at once (a big import, a
crafted code) wait for a click. Game ids follow OpenFront's shape (8-10
letters and digits; a bare word must also contain a digit or mixed case, so a
pasted chat line adds nothing). A game not published yet gets Retry.
node tools/test-tournament.mjs tests link parsing, matching, scoring,
tie-breaks, the bracket, share codes, storage merging and migration, and the
validation over the real records in .recap/; node tools/shot-tournament.mjs
screenshots every view through the launcher.
When the end screen appears, a panel opens bottom-right with what happened in
the game you just played, from OpenFront's own record of it
(api.openfront.io/public/game/<id>) read against everyone's world percentile.
- Result banner -
Victory,#33 of 71,Top 8 of 74(alive at the end),Defeat(1v1), orTeam Yellow won; when you fell and what share of the lobby you outlasted; your percentile badge. The coloured edge says how it went before you read anything. - Your numbers, each with its rank in the lobby when that rank is a good one: players conquered, troops sent, gold, cities captured, nukes landed, trade ships captured, warheads shot down, structures built.
- Players alive - the survival curve of the whole lobby with your exit marked on it.
- Story lines - who eliminated you (when the record says so), seeded #9 of 45 by world rank, finished #1 among them (+8), the higher-ranked players you outlasted, an upset or "the favourite delivered", your share of the winning team's troops, clan-mates in the same game, and how strong the lobby was.
- Graphs tab - you against the winner (or the best of the rest, if you won) and the lobby median on gold / troops / conquests / structures; where your gold came from (workers, conquest, trade, trains, piracy); the lobby's rank bands with yours marked.
- Awards tab - Warlord, Executioner, Tycoon, Merchant prince, Pirate king, Rail baron, Atomic enthusiast, Doomsday, Iron dome, Admiral, City snatcher, Architect, Turtle, Most wanted, Backstabber, Last to fall. One holder each, above a floor, ties award nobody, at most two per player (plus Clean sweep for five or more).
- Standings tab - everyone, with badges, time of elimination, conquests and gold.
- Footer: your world rank before -> after, and today's session.
- Share image - a 1200x630 PNG (the size Discord unfurls) with the result, your numbers, the survival curve and the top awards, drawn on a canvas in your theme and copied to the clipboard; falls back to a download. It names other players only for compliments. Copy text copies the same as lines.
It never sits on the game's own dialog: if it would overlap the win modal (small windows, large UI size) it folds to its title bar, which then carries the headline; click to open. The fold state is remembered.
What the record can and cannot say (checked against OpenFront's source,
src/core/StatsSchemas.ts, and real records - node tools/test-recap.mjs):
- A player with no stats never spawned: not ranked, not counted in "of N".
- No elimination tick means still had land at the end, not won. Usually
several players do, and the record cannot order them, so they are shown tied
(
=2, "Top 8"). Newer records carryfinalTiles; then the places are exact. - In a 1v1 the loser usually has no elimination tick at all.
- A team game lists only the winning team's members (older records: only those alive at the end). Nobody else's team is recorded, so a player off that list is told who won and what happened to them, never "defeat".
- A nation can win; then no human did, and places start at 2.
- Troop numbers are stored x10; conquests count conquer events, so they are "players conquered", never "kills"; trade gold is credited to both ends of a route, so it is never summed across players; "landed" can exceed "launched", so there is no accuracy stat.
- Usernames are not unique. Two players with your name and no clan tag to tell them apart means the recap shows the game as a spectator would see it.
- The record is archived when the game ends. The win screen also appears when you are eliminated while the game goes on, so the recap polls quickly for a minute, then every 30 s while that game is still on screen (up to about half an hour). A game still unpublished when you leave is asked for again on your next openfront.io page, for up to a day, so today's session stays complete.
- Streamer mode shows you as "You" everywhere, including the image.
While you play, the extension takes a small picture of the whole map - who owns what, independent of where your camera is - every few seconds, by reading the game client's tile buffer and terrain, and every player's name, colour and land share for the strip (page world, read-only: nothing is written, no intent is sent). It records only when the recap can show it: the recap switched on, rank lookups agreed, and the Timelapse switch on. The recap's Timelapse tab plays it back with a strip under the map: game clock, humans still alive, your share of the land, and the three biggest empires. The tab is there while the game's record is still awaited, and for games whose record is never published (single-player), too. From there:
- Save video - WebM, a few hundred KB; Discord and browsers play it inline. Ends on your recap card.
- Save GIF - works everywhere, bigger. Own encoder (
src/timelapse.js): one 256-colour palette, and from the second frame on only the box that changed is stored with unchanged pixels transparent, which is most of the size on a map.node tools/test-gif.mjsdecodes what it writes with an independent decoder and compares pixel for pixel.
Frames are kept in memory, in that tab only, at most 360 (long games keep every other frame and slow down), and are thrown away with the next game or when the tab closes. Nothing is uploaded: saving writes a file on your computer. Streamer mode leaves other players' names out of the strip; the names in it are the ones the game shows you (with OpenFront's Hidden Names on, hidden). The stream overlay can play it as a replay when the game is over (below). Switch: Game -> Timelapse.
An OBS overlay page (src/overlay.html), made of cards rather than text:
- Rank - your world-rank gauge (with today's drift as an arrow inside it), your win streak as flames and today's games as pips (a win filled), wins/games.
- Live game - state (live / spawning / out / game over), map and mode, the game clock, your place, your land share, humans alive, and bars for the three biggest empires against yours (no names).
- Recap - when the game's record is in, the recap's share card for a set number of seconds (then the live card of the next game takes over).
- Replay - after the recap card, the game's timelapse as an animated GIF for a set number of seconds, then the overlay hides (see below).
- Caster - observer mode: the leaderboard, teams and eliminations of a game you watch, shown late on purpose (see Observer mode).
Browser extension. Settings -> Tools -> Streamer overlay -> Open opens
it in a small window of its own on green. In OBS: Window Capture -> that
window -> filter Chroma Key (green). Its settings panel (the gear on hover,
or ?edit=1) picks the cards, corner, backdrop, size, how long the recap
stays, whether your name shows, and has sample data for placing it; the choices
live in the address, so a reload keeps them. OBS cannot load extension pages
itself; the panel's Copy address is for browser-source plugins that can.
Keep the window uncovered: Chrome stops painting a window that is fully hidden.
Steam launcher. The launcher prints the overlay's address (and its settings page has Copy under Tools): OBS -> Browser Source -> that address, 1920x1080, backdrop transparent. The address contains the launcher's secret key: never show it on stream or share it. It changes every time the launcher starts, so paste the new one into OBS after a restart.
Options in the address: w=rank,live,recap (which cards, in that order;
caster is the fourth), bg=transparent|green|dark, pos=tl|tr|bl|br,
scale=0.5..3, recap=20 (seconds; 0 = until the next game), replay=25
(seconds of replay after the recap card; 0 = off), delay=0..600 (seconds
everything about a game runs late - the game cards, the recap and replay, and the
rank card's pips, streak and rank after a game; left out of the address it is
90 with the caster card or whenever the game on the overlay is one you watch,
whatever the cards, and 0 otherwise; a delay= in the address always wins, 0
included), name=0 (never show your name), streamer=1. Streamer mode always wins: with it on, neither your name nor
your rank is shown (no gauge, no drift arrow; the streak and today's games
stay), as everywhere else in the extension. With streamer mode or name=0 the
recap card is drawn masked too ("You", no rank); an unmasked card is never shown
on such a page.
How it works, all on this computer: while the overlay page is open it writes a
heartbeat (overlayEnabled, a time, every 20 s; 0 when it closes, and
overlayMask while it hides your name) to chrome.storage.local. Only while
that is fresh (2.5 minutes) does the openfront.io tab publish: your ofstats name
(overlaySelf), the running game (overlayLive: counts and shares, no names,
from page-probe.js, read-only; written when it changes - the clock does not
count, the page runs it - and every 4 s otherwise; removed when you leave the
game) and the recap card once per game (overlayRecap, a 1200x630 PNG). With
several openfront.io tabs open, one publishes the game at a time: the one that
started, or the one on screen. A few seconds after the overlay page is gone the
tab removes all of it. The page reacts to storage changes and asks the worker
for your own rank like the home card does (right after a game, bypassing the
10-minute cache). The recap card needs the recap on and rank lookups agreed,
like the recap itself (closing the recap panel does not stop the card; a
replay's end screen never makes one). The openfront.io page can see that an
overlay is open (data-ofr-overlay="on" on its root element). node tools/test-overlay.mjs
checks the page logic; node tools/shot-overlay.mjs runs the whole path against
a stand-in game through the launcher and takes the screenshots.
Replay. The timelapse's frames live only in the game tab, so that tab makes
the GIF: once the game is over (the client says so; never while it is being
played, and never for the replay of an old game - the client's replay flag, or a
record that ended more than three hours ago), while an overlay page is open, it
encodes the recorded frames with the same encoder as Save GIF, a few frames at
a time so the page stays responsive, and hands the result over as
overlayReplay - at most 1.5 MB of storage (about 1.1 MB of GIF; every storage
listener - the worker, each openfront.io tab, the launcher's pages - is handed it
with each change): fewer frames first, then the recorded size instead of twice
it, until it fits; if it never does, the overlay's settings say the last replay
was skipped. It is written once per game (again only when the masking flips),
and the previous game's recap card and replay are removed before a new game's
go in, so storage never holds two games' images and no change event carries an
old and a new one. Masked exactly like the recap card: with
streamer mode on or an overlay page that hides your name, the strip under the
map names nobody, and it is made again when that flips. It is removed with the
rest when the overlay closes. The overlay plays it right after the recap card
(replay= seconds, 0 = off, a switch in its settings), then hides; with
recap=0 the card stays and there is no replay. It needs the Timelapse switch
(and the recap, and rank lookups agreed) while the game runs. The launcher keeps
it in memory only, like the other overlay keys.
Watch someone else's game - a tournament final, a friend's match - with a caster view.
Observer page (Settings -> Tools -> Observer). Paste a game link, a
lobby link (#join=..., /join/...) or just the id, press Check. The
extension asks OpenFront's own public endpoints whether that game is there, the
same way the site's lobby window does (the id's first letter names the server,
its hash the worker; /api/game/<id>/exists and /api/game/<id>) - only the id
is sent, only with rank lookups agreed - and shows map, mode, players and state:
- Live - Watch opens the game's link in a new tab. OpenFront seats anyone who opens a running game's link as a spectator. Watch waits until 9 s after the start time, by OpenFront's own clock: the server really starts the game 2-3 s after that time (a join before it is a lobby join, as a player), and for 5 s after the real start it refuses any join that is not a spectator's (you are not seated at all). It is offered only with somebody playing and the server's time in the answer.
- In the lobby / Starts in 0:42 - not opened by itself: opening a lobby that has not started joins it as a player, unless you choose Spectate in the lobby yourself. The page checks once more by itself when the countdown ends, however long it is. Remind me when it starts checks again every 15 s (for up to 30 minutes), then offers Watch and sends a notification (with Sound and notification on); Open lobby anyway is there if you want the lobby. The reminder stops by itself when there is nothing to wait for (over, not found, an old id), and runs once per game however many observer tabs wait for it (the newest takes over; one notification).
- State unknown - no start time: a private lobby waiting for its host, or a lobby that started when it filled up (OpenFront sets no start time then, and its answer has no "started" flag). A lobby seen full counts as started from then on (the server's "reached max players" never goes back). Check again; Remind me stops after 8 checks that still cannot tell.
- No one is playing - past its start time with nobody connected as a player: OpenFront does not start a game without players, and a join could seat you as one. No Watch.
- Game over (it is in OpenFront's archive), Not found, or an old 8-character id (games running now have 10).
The extension never clicks anything on openfront.io, never picks Spectate for you and sends no game message: it only opens the link when you press a button.
Caster panel (in the game tab, whenever you are a spectator; Game ->
Caster panel). "Spectator" is read the way the server decides it: a replay,
Spectate picked in the lobby, or this client not on the game's roster (the
players frozen at the start). Not the client's own isSpectator(), which is also
true for every player during the spawn phase and for one who was eliminated: a
player never gets the panel, the standings or the rank lookups of the others. Docked on the right edge, foldable to a tab: the clock and
humans still in, every empire by land with a bar, its colour on the map and the
rank badge of known players (from the same lookups and cache as everywhere,
only with rank lookups agreed), a land split and alive pips per team in team
games, and who was eliminated when (humans and nations; bots are left out).
Names are the ones the game shows you - with OpenFront's Hidden Names on
they stay hidden - and streamer mode leaves players' names and ranks out. The
page script sends these standings only while the panel or an overlay wants them
(data-ofr-caster="on"), read-only, like everything else it reads.
Caster overlay (the observer page's Open caster overlay, or w=caster in
the overlay's address). The same leaderboard, team split and last eliminations
for OBS, then the game's replay when it is over. It runs 90 seconds late by
default (delay=, 0 to 600): viewers of a live game can tell the players in it
what they see (ghosting); holding the overlay back makes what it shows useless
to them. The page keeps what it receives in its own memory and shows the state
from delay seconds ago, with a small "delayed 90s" in the corner; the recap
card comes when the delayed stream reaches the end of the game (the game's card
runs until then), and the rank card's pips and streak move that late too. The
default cards get the same 90 s whenever the game on the overlay is one you
watch. It only delays the overlay: delay the game capture too (OBS: stream
delay, or a Render Delay filter on the game source). With streamer mode or
name=0 the card names nobody.
node tools/test-observer.mjs checks link parsing, the route to a game's server,
the status and reminder rules, the leaderboard and eliminations logic, the real
page-probe.js against a stand-in GameView with OpenFront's seat rules (a player
spawning or eliminated is not a spectator) and the worker's check against a
stand-in network; node tools/shot-observer.mjs runs
it all end to end against a stand-in spectator game through the launcher (the
observer page against a stubbed answer: it never asks OpenFront) and takes the
screenshots in .shots/observer/.
A chat between players who have this extension, one room per game - in the
lobby, after the game, and during team games. There is no server of
ours: messages are Nostr ephemeral
events (kind 20787: relays are meant to pass them on to whoever is subscribed
and not store them, and each carries a five-minute NIP-40 expiry - but nobody
can guarantee that a third-party relay, or anyone subscribed, keeps no copy) on
four public relays - relay.primal.net, nos.lol, nostr.mom,
relay.snort.social (node tools/probe-relays.mjs re-checks which relays
deliver). The room is a hash of the game id, so a lobby stays together when the
game starts. Game ids are public, so anyone can tell which game a room belongs
to.
What that means, plainly:
- The relays are third parties. They see your IP address and what you send. Other players see your name, your text and a per-game key (in team games also the player slot it claims and whom it verified) - not your IP (which is why this is relay-based and not peer-to-peer WebRTC).
- Names are not verified. Anyone can type any name, and nothing can prove a sender is the player of that name. Each name therefore shows a 4-character fingerprint of the sender's key, and no rank badge is ever attached.
- Messages are signed by a throw-away key made in your browser per game
(
chrome.storage.session: memory only, gone when the browser closes), so games cannot be linked through it and no key sits on disk. Every incoming event is checked - shape, size, room, clock (2 min), id, BIP-340 signature - before it is shown; a relay cannot forge, edit or rename a message. - Text only: no markup, links are not clickable, control / zero-width / bidi
characters and zalgo stacks are stripped. Slurs and the like are masked
(switchable).
xmutes a sender for that game; six messages in ten seconds mutes a sender for a minute; you can send one message per 1.2 s, twelve a minute. There is nobody to report to - no server - so muting is the tool. - During play: open in team games; paused while you are alive in a free-for-all: the tab leaves the room altogether - no relay connection, nothing shown, nothing sent (not even the presence signal), nothing received - and rejoins the same room once you are out or the game ends. OpenFront's terms forbid outside channels for coordinating in free-for-all. "Also while alive in a free-for-all" is a separate switch, off by default, with that warning on it. Typing in the box does not trigger the game's hotkeys.
- It turns on only after an explicit "I agree" in the toolbar popup that lists what is sent and to whom. When an update changes what chat sends (5.8 added the team channel), chat switches itself off once and asks again. No settings copy embedded in a page can switch it (or the free-for-all switch) on - not the in-page overlay on openfront.io, and not the companion launcher's overlay in the game window; they can only switch it off. A page script could cover such an overlay and steer a click onto it. In the launcher, chat is switched on from its settings page in your normal browser.
- The chat is public: anyone connected to those relays can read a room, not only users of this extension. While it is on (and not paused), your name is announced to the room about every 45-60 seconds even if you do not type.
- There is no server to report to, so muting is the tool.
- The page itself is not trusted: OpenFront's pages carry third-party ad
scripts, which share the DOM with any content-script UI. The panel lives in a
closed shadow root (page scripts cannot read messages or the input box) and
only real user input sends or mutes (
event.isTrusted). What the panel cannot protect is what the game tells it: see the team channel's limits below. - Streamer mode sends "Player" instead of your name. In team games the team channel still announces your player slot, and anyone in that game can match the slot to your name.
In a team game the panel gets a second tab, Team: the text of messages there is encrypted to teammates verified through the game - by you, or through a chain of teammates who verified each other (up to about eight steps) - at most 12 per message, and is shown under the name the game gives that player - not a typed one.
Why it needs verifying: everything in an OpenFront game is public (every client holds the whole state), so there is no secret only your team knows. What an enemy cannot do is act in the game as your teammate. So:
- You press Verify next to a teammate who has the extension; they press it for you. Nothing happens - nothing is even answered on the relay - without both clicks.
- The two extensions agree on three emojis with a commit-then-reveal exchange, so neither side, nor anyone in between, can steer which three.
- Each of you sends them to the other with the game's own emoji menu (hold Alt, click their territory; the game allows one every 5 s per recipient, the panel counts down). The extension never sends anything into the game - it watches its own copy of the game for exactly that run, from that player, to you, after the pairing started.
- Verified teammates vouch for the keys they verified, and those keys for the ones they verified (up to about eight steps), so a team needs a chain, not every pair. A key vouched for this way also receives your team messages.
What the relay room sees, once the team channel is on in a team game: from the start of the game and about every 45 s, with no click needed, your key announces which player slot (the game's per-game player number) it claims, the keys you verified, and any key that claimed to be you. Pressing Verify publishes a handshake naming that teammate's key and both player slots. Each team message lists its recipients' key prefixes and shows its length. Only the message text is encrypted.
An impostor gets one blind guess per click of yours: about 1 in 175 000. What it does not protect against:
- any script running in OpenFront's page, including the third-party ad scripts the site loads, and other extensions. The extension learns your team's roster and the emojis sent from the page's own game object, and such a script can fake that information. After you press Verify once on a key of its choosing, it can get that key verified as your teammate and then read the team channel. The same goes for the page of any teammate in the chain: a key verified there is trusted by you too, with no click of yours. No extension-side code can fully prevent this: the page owns the game objects;
- a teammate anywhere in the chain who leaks the channel, or vouches for an outsider;
- a teammate who is talked into sending three emojis by someone on voice chat (the panel says never to, and during team games shows no emoji from anyone else in the public tab, whether or not your team channel is on).
Who takes part is public: the relay room sees which keys claim which player
slot, who verified whom, and who wrote to which keys, when. Full protocol,
threat model and what a pre-implementation red-team review changed:
docs/TEAM-CHAT.md. node tools/test-team.mjs runs honest
pairings and the attacks. Switch: Chat -> Encrypted team channel (on by
default once chat is on).
Crypto is @noble/curves + @noble/hashes (MIT, audited; Schnorr signatures and the
team channel's ECDH - AES-GCM and HKDF are the browser's WebCrypto), vendored as one
unminified file by node tools/build-vendor.mjs, which refuses to install a bundle that
fails BIP-340's test vector. node tools/test-chat.mjs [--live] covers the
event rules and text hygiene, and with --live two clients through the real
relays.
Usernames are not unique, so the recap used to refuse to guess when two players shared yours. It now uses OpenFront's own ids:
- client id - the public, per-game id the page already has (the lobby
modal's
currentClientID, ormyPlayer().clientID()in game; read bypage-probe.js). The game record lists it per player, so the match is exact. - public id - the stable
publicIDthe record prints for every player. Read from OpenFront's own page (page-probe.js: the account answer the page fetched, on adata-ofr-meattribute) or learnt from a record matched by client id, stored locally, and used when the client id is not known (a game settled later, a reloaded tab). - the name is the last resort, with the old rule: two matches and no clan tag to tell them apart means no guess.
The persistent id is a secret (it is the anonymous account) and is never read.
src/charts.js is a small SVG/DOM chart kit - gauges and rings, meters, pips,
mirrored comparison bars, step/line/area, columns, grouped comparison bars,
stacked bar, band histogram, icons - no library, since a content script cannot
load one from a CDN. Marks carry classes, colours come from the theme tokens.
It draws the recap, the home card and every dashboard section; in Trends,
for instance, the rolling 10-game win rate against what an average player would
win in the same lobbies, how much of each game you survived, and gold per game.
"Website layout" in the settings re-arranges OpenFront's own front page. The
site is Tailwind utilities throughout, so the only stable handles are its custom
elements and the structure around them (main > div, game-mode-selector > div,
desktop-nav-bar > nav, div:has(> streaming-now)); src/site-layouts.css
documents each. Measured on the live page at 1440px:
| Template | What changes | Content column |
|---|---|---|
| Default | nothing | 756px (the site caps it at 20cm) |
| Wide | lifts the cap; featured map 3:2 with upcoming lobbies two abreast | 1339px |
| Sidebar | nav becomes a fixed 232px left rail, PRO tag on the logo at the top, account/bell at the bottom | 1107px |
| Focus | one 680px column; news, streams and promos hidden; play buttons directly under the featured lobby, upcoming below | 680px |
None produces horizontal scroll. Ads are left alone in every template: they
fund the game. One trap worth recording: the site pins the play grid's second
row at 360px, so re-ordering rows in Focus needs grid-template-rows: none or
the button row inherits a 360px track.
Separate from the website layout: the Pro card can be Cards (the default:
rank gauge beside your name, win-rate bar and last-10 strip), Compact (the
same card with a smaller gauge and tighter spacing; the dashboard tightens too)
or Panel (docked right, dashboard as a side drawer),
and everything the extension draws scales with Size (85% / 100% / 120% /
140%, via zoom on its containers). The card sits directly under the username
strip; it used to be inserted inside that strip's flex row, which squeezed the
name field down to "T...".
The settings open in the page as an overlay (the popup's own document in an
iframe, so it keeps full chrome.* access) from three places: a Settings
button on the Pro card, another on the dashboard bar, and an "OpenFront Pro
settings" entry added to OpenFront's account dropdown. That dropdown only
exists while open and only for signed-in users, so the entry is (re)added
whenever the menu is on screen without it; it copies a neighbouring item's
classes to look native, and stays above a destructive last item (log out).
What talks to third parties - chat and its free-for-all switch - and the
auto-copy (which needs a browser permission) cannot be switched on from this
overlay, only off; that happens in the toolbar popup.
Chrome does not run declared content scripts when the extension's site access is
set to "on click", and it skips tabs that were already open when the extension
was reloaded. Both look identical to a broken extension. So the worker also
injects on every openfront.io navigation and into already-open tabs at
install/startup. content.js is wrapped in a guarded IIFE, so arriving twice in
one page is a no-op rather than a redeclaration crash.
The popup reports what is actually happening — worker state, ofstats
reachability, site access, and the last scan. Site access and the last scan are
read directly (from Chrome and from storage), not through the worker, since the
worker is one of the suspects. ofstats reachability is asked of the worker,
which contacts api.ofstats.io only once rank lookups are agreed and switched
on; until then the line reads "ofstats.io: not checked". Install only one
copy: two copies each run their own click
listener, which handled each badge click twice (now deduped by stamping the
click event's timestamp on the badge, but the badges still double up).
The tooltip adds Met 3 times recently — you won 1, they won 2 when your last
60 games and theirs overlap. It costs no extra requests: the game ids come from
the same player payload, and the viewer is identified from the lobby's own
"current player" row (falling back to OpenFront's stored username and clan tag).
Clicking any rank badge opens that player's stats in the page (the Pro dashboard); Shift+click toggles the watchlist instead.
LobbyPlayerView is a Lit element with createRenderRoot() { return this }, so
the lobby list lives in the light DOM and a content script can read it. Element
properties set by page scripts are not visible across the isolated world, so
names are parsed from direct text nodes of the name elements, for all three list
shapes (free-for-all tags, assigned team cards, unassigned column).
Results are cached in chrome.storage.local — 10 min for a hit, 30 min for a
miss or a failed request — and lookups are queued at 6 concurrent requests, so
a 50-player lobby does not hammer the tracker.
Reloads and updates. When the extension is reloaded, the worker injects a
fresh copy of the content script into open tabs, but the copy that was already
running is never unloaded: its listeners keep firing with frozen settings and a
dead chrome.*. Every entry point therefore checks alive() (is there still a
runtime id?) and an orphan stands down. Click handling is delegated on window
in the capture phase, which runs ahead of document, so the live copy also gets
to a click before any document-level listener left by an older build, and stops
it there. tools/cdp-badge.mjs exercises exactly this: reload the extension
with the tab open, then click the PRO tag and a rank badge.
manifest.json
src/background.js ofstats lookups, cache, request queue, auto-injection,
game records, consent gate, chat relays and team glue
src/themes.js THE theme catalogue: names, icon sets, site palette recipes
src/scoring.js percentile scoring, shared by badges, dashboard and tools
src/charts.js SVG/DOM chart kit, themed by tokens
src/dashboard.js Pro dashboard and home card, drawn with charts.js (exact
figures in folded Numbers boxes)
src/recap.js game recap: analysis, panel, share image
src/welcome.html/js first-run disclosure and consent
src/clans.html/js clan hub: weekly table, clan page, clan vs clan, recruits
src/clans-logic.js the clan hub's pure logic: weeks, movers, comparison, head-to-head, recruits
src/overlay.html/js OBS stream overlay (rank, live game, recap card, replay, caster card)
src/overlay-core.js the overlay's pure logic: options, checks, what shows, delay buffer
src/observer.html/js observer page: check a game link, watch, remind me when it starts
src/observer-core.js observer mode's pure logic: links, route, status, caster leaderboard and eliminations
src/caster.js the caster panel shown in the game tab while you watch
src/tournament.html/js/css tournaments: setup, games, standings, bracket, series, share image
src/tournament-core.js the tournaments' pure logic: links, matching, scoring, bracket, share links, validation
src/chat.js chat panel (closed shadow root), muting, filter, presence, Team tab
src/team.js team channel: teammate verification, trust, encryption (worker)
src/timelapse.js timelapse frames, preview, WebM and GIF export
src/nostr.js minimal Nostr client: ephemeral events, verification, relay pool
src/vendor/ nostr-crypto.js - vendored @noble Schnorr + ECDH + SHA-256
src/dashboard.css dashboard, home card and settings overlay styling
src/site-layouts.css the three website layout templates
src/page-themes.css GENERATED from themes.js: OpenFront's palette per theme
src/page-probe.js MAIN-world probe: map preview, your player id, timelapse frames, team roster/emoji feed, overlay figures, caster feed
src/map-viewer.js full-screen zoomable terrain view
icons/ extension and notification icons
sounds/alert.wav watchlist alert
src/content.js DOM scanning (lobby, hover panel, leaderboard), badges,
summaries, threat marks, map preview
src/content.css tokens + theme blocks; badges, summaries, charts, recap
src/popup.html/js settings
tools/calibrate.mjs re-derives the percentile table from live data
tools/gen-page-themes.mjs regenerates src/page-themes.css, checks theme consistency
tools/check-theme-contrast.mjs contrast and band-distinctness check for every theme
tools/test-recap.mjs recap analysis over real game records, in node
tools/test-overlay.mjs stream overlay logic, in node
tools/shot-overlay.mjs stream overlay end to end (stand-in game + launcher) and screenshots
tools/test-observer.mjs observer mode logic and the worker's game check, in node
tools/shot-observer.mjs observer mode, caster overlay and replay end to end (stand-in spectator game + launcher), screenshots
tools/standin-game.mjs the stand-in game pages those two use (a fake, read-only GameView)
tools/test-clans.mjs clan hub logic and the worker's clanTable route, in node
tools/shot-clans.mjs clan hub screenshots with real ofstats data, through the launcher
tools/test-tournament.mjs tournament logic over the real records in .recap/, in node
tools/shot-tournament.mjs tournament screenshots (made-up tournaments from .recap/), through the launcher
tools/tournament-fixtures.mjs shared by those two: loads the logic, the records and the demo tournaments
tools/test-chat.mjs chat: event rules, text hygiene, --live relay round trip
tools/test-team.mjs team channel: pairings, trust, encryption, and the attacks
tools/test-gif.mjs GIF encoder against an independent decoder
docs/TEAM-CHAT.md team channel protocol and threat model
tools/probe-relays.mjs which public relays deliver ephemeral events
tools/build-vendor.mjs rebuilds src/vendor/nostr-crypto.js (checked against BIP-340)
tools/pack.mjs builds the Chrome Web Store zip (manifest, src/, icons/, sounds/)
tools/build-launcher.mjs builds the launcher downloads (exe, portable zip, checksums)
tools/make-store-images.py Chrome Web Store screenshots and tiles from .shots/
tools/make-icons.py draws the icon set (icons/, store/)
tools/asar-peek.mjs read-only look inside an Electron app.asar
launcher/ companion launcher for the Steam build
store/ listing text, privacy-tab answers, submission checklist
tools/cdp-*.mjs dev: drive a debug Chrome (screenshots, live checks)
test/ fixture page reproducing the lobby markup
To run the fixture: python -m http.server 8931 from this folder, then open
http://localhost:8931/test/index.html. It stubs the chrome API; live calls
from it fail with CORS by design.
The Steam (Electron) version cannot load extensions. launcher/ runs this same
code against it from outside, without touching the game's files (settings open
inside the game window) - experimental,
and with caveats worth reading first: launcher/README.md.
MIT - see LICENSE. Bundled third-party code and the services the extension talks to are listed in THIRD_PARTY_NOTICES.md. Unofficial: not affiliated with, endorsed by or sponsored by OpenFront.







