BeMapAPI
BeMap 4.1.0

BeNomad Tiles

Sessions & tokens#

One HTTP Basic login against the tiles worker returns a JWT valid ~1 hour, and every tile, style, glyph and discovery request must carry it, by cookie, header or query parameter.

You normally write none of this. bemap.MapLibreMap instantiates bemap.TilesAuth for you as soon as ctx.tilesHost is set: it logs in, picks the wire mode, waits for the token before the first tile request, renews ahead of expiry and recovers from a 401. This page is the contract underneath: read it when you integrate raw MapLibre, a native client, or a backend.

At a glance#

  • Login: POST {tilesBase}/api/login with Authorization: Basic base64(login:password) → { ok, username, token }, plus an HttpOnly session cookie.
  • Token lifetime ~1 hour. Limits: 100 req/s per user, Range ≤ 10 MB, HTTPS only.
  • Four wire modes on tilesAuth: auto (default), cookie, header, query.
  • auto resolves once, at construction: same registrable domain as tilesHost ⇒ cookie; cross-site ⇒ X-Session-Token header.
  • Renewal is automatic: proactive 5 minutes before the exp claim, reactive on 401, plus visibilitychange and offline handling.
  • The token is persisted per ctx.tokenStorage: 'session' (default), 'local' or 'memory', under the key bemap_tiles_token.
  • Credential-less integrations: a backend token provider, or the v2.0.2 proxy option.

Usage#

The supported path: set tilesHost and let the SDK own the session:

js
var ctx = new bemap.Context({
    login:     'YOUR_LOGIN',
    password:  'YOUR_PASSWORD',
    secure:    true,
    host:      'bemap.benomad.com',
    tilesHost: 'mptiles-api.benomad.net'
});

var map = new bemap.MapLibreMap(ctx, 'map');
map.isTokenValid();   // Boolean      map.getToken();  // String | null
map.refreshToken();   // Promise<String>

Pin the wire mode only when auto cannot be right for your deployment, for example a native WebView that cannot inject headers:

js
new bemap.Context({ tilesHost: 'mptiles-api.benomad.net', tilesAuth: 'query' });   // shorthand
new bemap.Context({ tilesHost: '…', tilesAuth: { mode: 'header', tokenHeader: 'X-Session-Token' } });
new bemap.MapLibreMap(ctx, 'map', { tilesAuth: { mode: 'cookie' } });              // per-map override

Raw HTTP: log in, then spend the token. Never inline the credentials:

bash
TOKEN=$(curl -s -X POST "https://mptiles-api.benomad.net/api/login" \
  -u "$LOGIN:$PASS" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')

# 200 while the session is valid, 401 once it has expired
curl -s -o /dev/null -w '%{http_code}\n' -H "X-Session-Token: $TOKEN" \
  "https://mptiles-api.benomad.net/api/status"

# a z/x/y MVT tile with the same token
curl -s -H "X-Session-Token: $TOKEN" \
  "https://mptiles-api.benomad.net/default/12/2074/1409.pbf" --output tile.pbf

Reference#

Session endpoints#

EndpointMethodAuth sentResponse
{tilesBase}/api/loginPOSTAuthorization: Basic base64(login:password){ ok, username, token } + Set-Cookie: session=…; HttpOnly; Secure; SameSite=None. Token TTL ~1 h
{tilesBase}/api/statusGETCurrent token, any wire mode200 session valid / 401 expired or unknown
{tilesBase}/api/logoutGET or POSTCurrent token, any wire mode200. Clears the cookie session. Does not revoke the JWT, see below

{tilesBase} is https://<tilesHost>: ctx.getTilesBaseUrl(), plus getTilesLoginUrl() / getTilesStatusUrl() / getTilesLogoutUrl().

Logout does not revoke the token. Verified against the deployed beta worker: /api/logout accepts both GET and POST (both return 200), but the JWT stays usable afterwards, /api/status still returns 200 and tile requests still succeed until the token reaches its own exp. Logout clears the HttpOnly cookie session; it is not a bearer-token revocation. Treat a JWT as valid for its full lifetime once issued, and keep it out of logs and URLs you persist. The SDK sends POST (bemap.TilesAuth.logout({ serverSide: true })); a raw client may use either method.

Limits#

LimitValueExceeded →
Token lifetime~1 hour401 on every subsequent request
Request rate100 req/s per user429
Range size≤ 10 MB per request403
TransportHTTPS onlyRequest refused

Wire modes: tilesAuth#

Set on the Context, overridable field-by-field per map. Accepts the string shorthand ('auto', 'cookie', 'header', 'query') or the full object. An unrecognised mode falls back to auto.

FieldDefaultMeaning
mode'auto''auto' | 'cookie' | 'header' | 'query'
credentials'include'Cookie mode only: 'include' | 'same-origin' | 'omit'
tokenHeader'X-Session-Token'Header mode only
tokenParam'token'Query mode only
modeHow the token rides the requestCost
auto (default)Resolved once, when bemap.TilesAuth is constructed: bemap.Context._sameSite() compares the registrable domain (last two labels) of window.location.hostname and tilesHost. Same ⇒ cookie; different ⇒ header. header is the safe fallback, so auto never silently breaks cross-site.None
cookiecredentials: <credentials> on every request; no custom header, no query parameter, so a Range GET stays a CORS-simple request.No preflight. First-party only: a SameSite=None cookie is blocked cross-site by Safari ITP, Firefox TCP and Chrome incognito.
header<tokenHeader>: <jwt> on every request.Works without cookies. Preflight is path-dependent: one cached OPTIONS per slice URL on the 200-slice path, one per request on the Range + no-store path.
query?<tokenParam>=<jwt> appended to the URL.No preflight, incognito-safe. Token appears in the URL; the browser cache refills on token rotation.

The resolved mode applies uniformly to the pmtiles Range fetch interceptor, MapLibre's transformRequest (style, glyphs, sprites, z/x/y), bemap.TilesStyle.fetch, and to login / logout.

bemap.TilesAuth(ctx, options)#

Constructed for you by bemap.MapLibreMap. Instantiate it directly only in a raw-MapLibre integration.

OptionTypePurpose
onErrorFunctionReceives a bemap.Error on any auth or network failure
onTokenFunctionReceives { reason, exp }: reason is 'login' or 'refresh', exactly one event per logical operation
tilesAuthObject | StringPer-map wire config, merged over ctx.tilesAuth field-by-field
MemberReturnsBehaviour
login([login], [password])Promise<String>Defaults to ctx.login / ctx.password. Concurrent calls join the single in-flight promise
whenTokenReady()Promise<String|null>Resolves with the current valid token, otherwise starts (or joins) a login. The synchronisation point the fetch interceptor uses
getToken()String | nullThe raw JWT
isValid()Booleanfalse with no token; true when exp is unparseable (assume valid until a 401); otherwise exp > now + 30 s
refresh()Promise<String>Proactive renewal with the original credentials; deduplicated
getHistory()Array<{exp:Number}>Last 3 superseded tokens: exp only, never the JWT
buildTransformRequest()FunctionMapLibre transformRequest; only stamps requests whose host equals ctx.tilesHost. Captures getToken, not the token value, so renewal is transparent
handle401(map, [styleSpec])Promise<String>Refresh, then setStyle(styleSpec, { _internal: true }) to retry pending tiles. N simultaneous 401s collapse into one refresh and one reload
logout([{ serverSide }])Promise<void>Always clears the token, history, storage and renewal timer. serverSide: true also calls the logout endpoint
destroy()-Clears the timer, removes the visibilitychange / online listeners, unregisters from the fetch interceptor
bemap.TilesAuth.STORAGE_KEYString'bemap_tiles_token'
bemap.TilesAuth.installFetchInterceptor(auth, tilesHost)-Scoped window.fetch wrapper that awaits login and stamps auth on requests to tilesHost. Idempotent; skips /api/login; needed because pmtiles.js bypasses transformRequest
bemap.TilesAuth.uninstallFetchInterceptor(tilesHost)-Unregisters the host; restores the original window.fetch when the registry empties

Token storage: ctx.tokenStorage#

ValueBacking storeLifetime
'session' (default)sessionStorageCleared when the tab closes
'local'localStorageSurvives reloads and restarts
'memory'noneNever persisted

Key bemap_tiles_token, value { token, exp }. Storage failures (private mode, quota, disabled) degrade silently to memory-only.

Error codes#

Raised through options.onError when you construct bemap.TilesAuth yourself. The map creates its own instance internally and routes those errors to an internal channel that 2.0.2 exposes no public subscription for: in practice you observe them in the console. See Map errors.

Conditionbemap.Error code
HTTP 401, or no tilesHost, or missing credentials, or a login response with no tokenUNAUTHORIZED
HTTP 403FORBIDDEN
HTTP 429RATE_LIMITED
Any other non-2xx, or a transport failureNETWORK
navigator.onLine === false at call timeOFFLINE

Notes#

Renewal timeline. After every successful login the SDK parses the JWT exp claim and schedules a refresh at exp − 5 minutes (clamped to [0, 24 h]); when exp cannot be parsed it falls back to a fixed 55 minutes. A 401 on a tile or style triggers handle401(): refresh, then a setStyle() reload so pending requests retry with the new token. On document.visibilitychange to visible the SDK re-logs in if the token has already expired, refreshes if it expires within 5 minutes, and does nothing otherwise. When navigator.onLine is false the renewal is queued and replayed on the next online event.

Credential-less: backend token provider. Your server holds the credentials, exposes one authenticated endpoint that forwards POST /api/login with the Basic header, and returns the JSON body unchanged; the browser receives only the short-lived JWT and fetches tiles directly from BeNomad. Only the tiles credentials move server-side, see The Context.

Credential-less: proxy (v2.0.2). new bemap.Context({ proxy: 'your-proxy.example.com', tilesHost: '…' }) routes the REST/WMS calls and the tiles login through your server, which injects the credentials; no Authorization ever leaves the browser. Tile bytes still go direct to tilesHost. Because the login response then comes from the proxy origin, the session cookie could never be sent to tilesHost, so the SDK forces the header wire, overriding an explicit tilesAuth: 'cookie' with a one-shot console warning ('query' is honoured). See The Context.

Gotchas#

  • tokenStorage values are 'session' / 'local' / 'memory', not 'sessionStorage' / 'localStorage'. An unrecognised value silently falls back to sessionStorage.
  • Never patch window.fetch yourself to inject the token. The SDK already owns a scoped, host-checked wrapper; a second patch double-stamps requests or drops the Range policy.
  • Pinning tilesAuth: 'cookie' on a cross-site app will 401 in Safari, Firefox and Chrome incognito. That is precisely what auto exists to avoid: leave it unset.
  • getHistory() is diagnostic only. It deliberately keeps just the exp claim, so logging it cannot exfiltrate a still-valid token. Never log getToken().
  • A missing login / password produces a one-shot console warning, then UNAUTHORIZED, not a silent blank map. Check the console before debugging CORS.
  • isValid() returns true when exp is unparseable. A non-JWT token is assumed good until the server answers 401; the fixed 55-minute renewal covers the rest.
  • logout() without { serverSide: true } only clears the client. The session stays valid on the worker until it expires.
  • A 403 on a .pmtiles request is usually not auth. No Range header, a Range over 10 MB, or a multi-part Range are all rejected with 403, see Troubleshooting.

See also#

  • Overview & access: the access contract in context, environments, integration paths
  • Web: PMTiles: where the token is attached on the browser path
  • Mobile & fleet z/x/y: the ?token= query wire for native clients
  • Cache & slices: why the wire mode changes the preflight and cache behaviour
  • Troubleshooting: 401 / 403 / CORS symptom to fix
  • The Context: tilesHost, tilesAuth, tokenStorage, proxy
  • Error handling: the typed-rejection pattern
  • Error codes: every bemap.Error constant