npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chrtco/sdk

v0.10.2

Published

Official TypeScript SDK for the CHRT nautical chart API

Downloads

1,075

Readme

@chrtco/sdk

Official TypeScript SDK for the CHRT nautical chart API.

Zero dependencies · Works in Node 18+, browsers, Deno, Bun · Full TypeScript types

Situational awareness only — not for primary navigation. CHRT is not an ECDIS and is not SOLAS-compliant. Do not use it as your primary means of navigation. Every chart CHRT serves comes from the issuing hydrographic office; keep the source attribution visible in anything you build.

Install

npm install @chrtco/sdk

Quick Start

import { Chrt } from '@chrtco/sdk';

// Read the key from the environment — never hard-code it in source.
const chrt = new Chrt(process.env.CHRT_API_KEY!);

// Get depth areas in a bounding box
const features = await chrt.features({
  bbox: [-71, 42, -70, 43],
  layer: 'DEPARE',
});

// Find buoys near a position
const nearby = await chrt.query({
  point: [-70.86, 42.02],
  radius: 2000,
  layer: 'BOYCAR,BOYLAT',
});

// List ENC cells — `source` takes the chart-source id ('NOAA_ENC'), not the
// agency name ('NOAA'), which matches nothing. chrt.coverage.list() lists the ids.
const cells = await chrt.cells.list({ source: 'NOAA_ENC' });

// Get a specific cell
const cell = await chrt.cells.get('US5MA11M');
console.log(cell.bbox_north, cell.bbox_west); // → 42.38 -71.05

API keys

Keys are issued in the CHRT platform under Settings → API keys and look like:

chrt_pk_live_<team>_<random>     chrt_sk_live_<team>_<random>
chrt_pk_test_<team>_<random>     chrt_sk_test_<team>_<random>
      ▲    ▲
      │    └── environment: live | test
      └── key type: pk (publishable) | sk (secret)

| | pk — publishable | sk — secret | |---|---|---| | Intended for | client-side code (browser, mobile app) | servers, CI, backend jobs | | Retrievable later | ✓ — re-reveal it in the dashboard | ✗ — shown once at creation; regenerate if lost |

pk and sk carry identical API privileges. The prefix records intent and whether CHRT keeps the plaintext for you to re-reveal — it is not a capability boundary. Any key you ship to a client can be extracted from it and replayed with full access.

So treat any key embedded in a browser bundle or mobile app as public: develop against a test-environment key, grant it only the scopes it needs, and rotate it if it leaks.

For a key you ship to a browser, set Allowed domains on it (platform → API keys). A browser request whose Origin isn't on the list is refused, so a key lifted from your bundle won't work on someone else's site. It is a browser-only deterrent, never a boundary — a non-browser caller sends no Origin and is allowed through, by design. Do not set the IP allowlist: it does not work, and setting it bricks the key on every real request path.

Spatial queries

query() answers "what's around me". Underway, the question is narrower.

// What's charted AHEAD — position + bearing, no route needed.
const fan = await chrt.ahead({
  point: [-70.9245, 42.33604],
  bearing: gps.cog,   // degrees true
  arc: 60,            // FULL sector width, so ±30°
  rangeNm: 3,
  safetyDepth: 3,     // omit if you set `vessel`
});

// What's charted along a PLANNED PASSAGE — ordered by how far in you meet it.
const passage = await chrt.alongRoute({
  route: [[-70.95, 42.33], [-71.0, 42.35], [-71.02, 42.36]],
  bufferNm: 0.5,
});
for (const f of passage.features) {
  const { along_m, distance_m, depth_clearance_m } = f.properties;
}

// Which CHARTS that passage needs — a different question, so a different call.
const { cells } = await chrt.coverage.forRoute({ route, bufferNm: 1 });

Neither replaces the other: a boat with a route still drifts, and COG rarely equals the route bearing — so a passage app wants alongRoute() once per route edit and ahead() continuously against the real track.

These tell you what is CHARTED, never whether it is safe. There is no severity score and no safe flag, deliberately — a risk model is a navigation-grade claim, and CHRT is situational-awareness only. You get facts that sort (distance_m, bearing_deg, along_m, least_depth_m, depth_clearance_m — negative means shallower than you declared safe) and your application owns the judgment.

An empty result is not an all-clear. It can mean nothing is charted there, or that CHRT has no coverage there at all. Every response carries a coverage block — check coverage.covered before telling a user anything.

if (!fan.coverage.covered) {
  // CHRT holds no chart for this sector. Say so — do not report "clear".
}

There is no speed parameter, by design: turning SOG into a lookahead distance is a navigational judgment that belongs to you. The usual recipe is rangeNm = SOG_knots × lookahead_hours, narrowing arc as speed rises. Re-fan on a meaningful course change rather than on a timer.

Maps (MapLibre GL JS / Mapbox GL JS)

CHRT gives you two ways to get an IHO S-52 style. They are not interchangeable — pick by whether your client runs JavaScript:

| | chrt.style() | chrt.styleUrl() / chrt.fetchStyle() | |---|---|---| | What it is | a JS function that builds the style locally | a URL CHRT serves (tiles.chrt.co/v1/styles/…) | | Network | none — works offline | one authenticated request | | Usable from | JavaScript only | any HTTP client (Swift, Kotlin, Python, curl) | | Custom style bound to your key | not applied | applied server-side (Dev plan and above) |

chrt.transformRequest() mints a short-lived tile token from your API key and attaches it to tile and served-style requests automatically. Examples use MapLibre — substitute mapbox-gl if you're already on Mapbox.

import maplibregl from 'maplibre-gl';
import { Chrt } from '@chrtco/sdk';

const chrt = new Chrt(process.env.CHRT_API_KEY!);

// Locally generated style — no network, no custom style applied.
const map = new maplibregl.Map({
  container: 'map',
  style: chrt.style({ palette: 'DAY' }),   // 'DAY' | 'DUSK' | 'NIGHT'
  center: [-70.25, 43.66],
  zoom: 12,
  transformRequest: chrt.transformRequest(),
});

Or use the served style, which honours the CHRTstyle custom style bound to your key:

const map = new maplibregl.Map({
  container: 'map',
  style: await chrt.styleUrl({ theme: 'dusk' }),   // 'day' | 'dusk' | 'night'
  transformRequest: chrt.transformRequest(),       // required — authenticates the style request too
});

styleUrl() is async: it pre-mints the tile token so the style request — which MapLibre does not retry — is guaranteed to carry auth. Always pair it with transformRequest().

Tiles are served from tiles.chrt.co and gated by a short-lived ES256 JWT. The SDK mints it from your key, attaches it as Authorization: Bearer, and refreshes it before expiry — you never handle tokens directly.

Symbol families

style(), styleUrl() and fetchStyle() all select symbol artwork, but the option is spelled differently on the local and served paths:

chrt.style({ symbolSet: 's101' });      // local  — symbolSet, palette: 'DAY' | 'DUSK' | 'NIGHT'
await chrt.styleUrl({ symbols: 's101' }); // served — symbols,   theme:   'day' | 'dusk' | 'night'

| Value | Artwork | |---|---| | s52 (default) | IHO S-52 Presentation Library — spec-exact ATON/LIGHTS selection | | s101 | official IHO S-101 Portrayal Catalogue artwork | | chrt | CHRT house set — colour-true silhouettes, capability badges | | raster | raster symbol set |

s101 and chrt need their images loaded at runtime via loadS101SymbolImages / loadChrtSymbolImages from @chrtco/sdk/atlas/browser.

Custom styles

chrt.style() returns a standard MapLibre style object you can mutate. For a fully custom look, build your own style over CHRT's vector source with buildSources() and keep chrt.transformRequest() for auth:

import { buildSources } from '@chrtco/sdk';

const sources = buildSources({ region: 'NOAA_ENC' });
// sources['NOAA_ENC'].tiles → ['https://tiles.chrt.co/v1/tiles/NOAA_ENC/{z}/{x}/{y}.mvt']
// Each S-57 class (DEPARE, LIGHTS, BOYLAT, …) is a `source-layer` inside that one source.

Using CHRT without JavaScript

Prefer codegen? The whole HTTP surface is described by an OpenAPI 3.1 spec at https://api.chrt.co/openapi.json — generate a typed Swift/Kotlin/Python/Go client from it instead of hand-rolling the calls below.

Native iOS/Android, Swift, Kotlin, Python and plain curl clients cannot call chrt.style() — it is a JavaScript function that builds a style object in-process. Use the served style API instead. It is two HTTP calls, no SDK required.

1. Mint a tile token (valid for a short window; re-mint before expires_at):

curl -X POST https://api.chrt.co/v1/mint/tile-token \
  -H "x-api-key: $CHRT_API_KEY" \
  -H "content-type: application/json" \
  -d '{"region":"NOAA_ENC"}'
{
  "token": "eyJhbGciOiJFUzI1NiIs…",  // ES256 JWT — do not log
  "expires_at": 1752600000,          // unix seconds, matches the JWT `exp`
  "kid": "chrt-tile-2026-04",
  "build_tag": "…",
  "region": "NOAA_ENC"
}

2. Fetch the style and the tiles with that token as a Bearer credential:

curl https://tiles.chrt.co/v1/styles/NOAA_ENC/style.json?theme=day \
  -H "Authorization: Bearer $TOKEN"

curl https://tiles.chrt.co/v1/tiles/NOAA_ENC/12/1234/1543.mvt \
  -H "Authorization: Bearer $TOKEN"

The returned document is a standard MapLibre style — hand it to MapLibre Native (iOS/Android), maplibre-gl, or any renderer that speaks the style spec. Point your renderer's request transformer at the same Authorization: Bearer header so tile requests carry it too.

The REST endpoints in the table below (/v1/features, /v1/query, /v1/query/ahead, /v1/cells, /v1/coverage) take the API key directly as x-api-key — no token minting needed.

Click-to-inspect (CHRTinteract)

Wires click → decode onto any MapLibre / Mapbox GL map showing CHRT tiles. Requires the Dev plan or above; the entitlement is enforced server-side at attach time.

const handle = await chrt.interact.attach(map, {
  onInspect: (result) => {
    // result.kind: 'navaid' | 'bridge' | 'area'
    // e.g. a light characteristic decoded to "Fl(2+1) W 10s", sector arcs,
    // bridge clearances, restriction lists, coordinates
    console.log(result);
  },
});

handle.detach();

With no onInspect/onCandidates callback, attach() renders a minimal built-in popup instead. Every result carries a notice — the non-SOLAS disclaimer, issuing-HO attribution and data currency. The built-in popup always renders them; if you build your own UI, render them too.

A chart click rarely hits exactly one feature (a buoy on a channel over a depth area), so interact collects every candidate under the click, dedupes them across tile boundaries by S-57 identity, and orders them point → line → area with safety-critical classes first.

// Headless candidate collection for any screen point or [lng, lat].
const { candidates, select } = await chrt.interact.inspectAll(map, [-70.86, 42.02]);
const result = await select(candidates[0]);

// Decode a single feature you already have.
const one = await chrt.interact.inspect(feature);

// Resolve full S-57 source properties + cell provenance (edition, update number,
// issue date). Metered against your `featureCalls` quota.
const enriched = await chrt.interact.inspect(feature, { enrich: true });

Configuration

const chrt = new Chrt({
  apiKey: process.env.CHRT_API_KEY!,
  baseUrl: 'https://api.chrt.co/v1',      // default
  retries: 2,                             // retry on 5xx + network errors, default 2
  region: 'NOAA_ENC',                     // default tile region
  tileBaseUrl: 'https://tiles.chrt.co',   // override for staging tiles
  deviceId: 'my-app-install-uuid',        // required for offline.* — see below
  manifestPath: './chrt-offline-manifest.json', // optional — enables offline budget + list()
  vessel: { safetyDepthM: 3 },            // one safety depth for every depth-aware call
});

new Chrt('chrt_sk_live_…') is shorthand for new Chrt({ apiKey: 'chrt_sk_live_…' }).

vessel — one safety depth

Set your draft-plus-margin once and every depth-aware call inherits it: ahead() and alongRoute() filter at it, and style() shades soundings and draws danger rings at it. An explicit per-call safetyDepth / safetyContour always wins.

It is client-side and per-instance, deliberately — not bound to your API key. A consumer app has one key and many skippers with different boats; a key-scoped draft would give them all the same number. Construct a Chrt per user, or pass safetyDepth per call.

With no vessel, CHRT sends no depth at all rather than guessing a draft — filtering on an invented one would silently drop real hazards.

Not yet reachable from the served style: styleUrl() takes theme + symbols only, so a non-JS client cannot currently get per-user depth shading.

API Reference

Which methods can a non-JS client use? Everything under HTTP endpoint below is a plain REST call any language can make. Everything marked JS-only runs in-process and has no HTTP equivalent.

| Method | HTTP endpoint | Notes | |---|---|---| | chrt.features(opts) | GET /v1/features | | | chrt.query(opts) | GET /v1/query | | | chrt.ahead(opts) | GET /v1/query/ahead | what's charted in a sector ahead | | chrt.alongRoute(opts) | GET /v1/query/route | same, for a planned passage | | chrt.alongRouteIter(opts) | GET /v1/query/route | auto-paginates | | chrt.cells.list(opts?) | GET /v1/cells | | | chrt.cells.get(id) | GET /v1/cells/{id} | | | chrt.coverage.list(opts?) | GET /v1/coverage | | | chrt.coverage.forRoute(opts) | GET /v1/coverage/route | which cells a passage needs | | chrt.warmTiles(region?) | POST /v1/mint/tile-token | | | chrt.styleUrl(opts?) | GET {tileBaseUrl}/v1/styles/{region}/style.json | mints first | | chrt.fetchStyle(opts?) | same, then fetches it | | | chrt.interact.entitlement() | POST /v1/interact/entitlement | | | chrt.style(opts?) | — | JS-only — local generator | | chrt.transformRequest(opts?) | — | JS-only — returns a closure | | chrt.tiles.tileUrl(z, x, y) | — | JS-only — string builder | | chrt.interact.attach/inspectAll | — | JS-only — needs a GL JS map | | chrt.featuresIter / cellsIter | — | JS-only — async iterators |

chrt.features(options)

Query GeoJSON features by bounding box.

| Option | Type | Required | Description | |--------|------|----------|-------------| | bbox | [w, s, e, n] | ✓ | Bounding box | | layer | string | | S-57 layer code (e.g. DEPARE) | | limit | number | | Max results (default 100, max 1000) | | offset | number | | Pagination offset | | signal | AbortSignal | | |

Returns a GeoJSON FeatureCollection with total_count, limit and offset alongside features.

chrt.query(options)

Spatial proximity search around a point.

| Option | Type | Required | Description | |--------|------|----------|-------------| | point | [lng, lat] | ✓ | Centre point | | radius | number | | Radius in metres (default 500, clamped 1–50 000) | | layer | string | | Comma-separated layer codes | | limit | number | | Max results (default 50, max 500) | | signal | AbortSignal | | |

Returns a QueryResponse: a GeoJSON FeatureCollection whose properties carry layer, cell_id and distance_m (geodesic metres from point), plus a query block echoing what the server actually applied after clamping — read res.query.radius_m to see that radius: 90000 silently became 50000. Unlike chrt.features(), this endpoint is not paginated: it returns no offset, so widen radius rather than paging.

chrt.ahead(options)

The fan-ahead query — the significant features in a sector from a point along a bearing. Built for the position-plus-COG case: a craft with a heading but no planned route.

| Option | Type | Required | Description | |--------|------|----------|-------------| | point | [lng, lat] | ✓ | Origin | | bearing | number | ✓ | Degrees true; out-of-range values normalise | | arc | number | | Full sector width centred on bearing (default 90 — arc: 60 sweeps ±30°) | | range | number | | Metres (default 5000, clamped 1–50 000) | | rangeNm | number | | Nautical miles — mutually exclusive with range (passing both throws) | | safetyDepth | number | | Filters out only what is known to be deeper | | layer | string | | Defaults to CHRT's significant-feature set | | limit | number | | Default 50, max 500 |

Each feature carries distance_m, bearing_deg, least_depth_m and depth_clearance_m (negative = shallower than your safetyDepth).

Facts that sort, never a verdict. There is deliberately no severity score and no safe flag — a risk model is a navigation-grade claim. You sort; CHRT ranks nothing.

An empty result is not an all-clear. Zero features in covered water and zero features because CHRT has no cells there are different answers. Every response carries a coverage block (covered, covered_fraction, cells) — check coverage.covered before telling a user anything.

chrt.cells.list(options?)

List available ENC cells.

| Option | Type | Required | Description | |--------|------|----------|-------------| | source | string | | Chart-source id, e.g. 'NOAA_ENC'not the agency name 'NOAA', which matches nothing and returns an empty list rather than an error. chrt.coverage.list() enumerates the ids. | | status | string | | e.g. 'CURRENT'. Case-insensitive. | | scaleBand | ScaleBand | | 'Overview' | 'General' | 'Coastal' | 'Approach' | 'Harbor'. Matched case-sensitively'harbor' matches nothing. | | bbox | [w, s, e, n] | | Cells whose bounds intersect this box | | limit | number | | Max results (default 100, max 1000) | | offset | number | | Pagination offset | | signal | AbortSignal | | |

Returns { cells, total_count, limit, offset }. Use chrt.cellsIter(options?) to page automatically.

chrt.cells.get(cellId)

Get metadata for a specific cell. Takes the id positionally: chrt.cells.get('US5MA11M'). Returns a CellDetail:

| Field | Type | Description | |-------|------|-------------| | cell_id | string | e.g. 'US5MA11M' | | title | string \| null | Chart title | | chart_source | string | Source id, e.g. 'NOAA_ENC' | | status | string | e.g. 'CURRENT' | | scale | number \| null | Compilation scale denominator | | scale_band | string \| null | Usage band — a ScaleBand name, e.g. 'Harbor' | | edition · update_number · issue_date | | Data currency — the fields that say how current the chart is | | bbox_north · bbox_south · bbox_east · bbox_west | number \| null | Bounds in decimal degrees (WGS 84), null if the issuing office recorded none. Written as a group, so in practice all four are set or all four are null. | | last_checked_at | string \| null | ISO 8601 — when CHRT last checked the issuing office's catalogue for this cell. Freshness of the check, not of the chart; use edition / update_number / issue_date for chart currency. |

Bounds are flat here and on coverage.forRoute() — one shape across the API. Up to 0.9.2 the SDK declared these flat fields while /v1/cells alone returned a nested bbox object, so cell.bbox_north typechecked and read undefined on every response. The endpoint was the outlier; it now sends what this type has always declared, and reading cell.bbox_north works against any deployed CHRT API.

chrt.coverage.list(options?)

List the chart sources available to your key. Options: country (e.g. 'US'), source. Returns { sources, total_cells }, where each source carries source (the id you filter cells by, e.g. 'NOAA_ENC'), agency (the issuing office's display name, e.g. 'NOAA'), cell_count and scale_bands (counts keyed by band name). This is the endpoint that tells you which source ids are valid — passing an agency name anywhere else silently matches nothing.

chrt.style(opts?)JS-only

Returns a ready-to-use MapLibre/Mapbox GL S-52 style object. Generated locally: no network, works offline, and does not apply a custom style bound to your key. Options: palette ('DAY' | 'DUSK' | 'NIGHT'), symbolSet, region, depthUnits, safetyContour, chartMode, plus the other atlas style options. Non-JS clients want styleUrl() instead.

chrt.styleUrl(opts?)

URL of the served S-52 style — the CHRTstyle Style API at {tileBaseUrl}/v1/styles/{region}/style.json. async: it pre-mints the tile token so the style request (which MapLibre does not retry) carries auth. Options: theme ('day' | 'dusk' | 'night'), symbols. Always pair with transformRequest().

The served document honours the CHRTstyle custom style bound to your key (a Dev+ feature; Free keys and keys with no bound style get the default themes). This is the style path for any client that can't run chrt.style().

chrt.fetchStyle(opts?)

Fetches the served style as a MapLibre style object. Same options as styleUrl(), plus signal. Prefer it over styleUrl() when you want to inspect or mutate the document, or when you can't use transformRequest() for the style request itself. Throws ChrtError on failure; does not retry.

chrt.transformRequest(opts?)JS-only

Returns a transformRequest closure that attaches the tile JWT as Authorization: Bearer on tiles.chrt.co tile and served-style requests only. Sprite, glyph and third-party URLs pass through untouched. Pass { region } to override the default region.

chrt.warmTiles(region?)

Pre-mints the tile token so you can surface auth errors (region_not_licensed, tile_quota_exceeded, …) before the map loads. Optional — transformRequest() warms the cache on its own.

chrt.tiles.tileUrl(z, x, y)JS-only

Concrete vector tile URL at tiles.chrt.co for one tile. Prefer chrt.style() / chrt.styleUrl(), which wire the source for you.

chrt.interact.*

See Click-to-inspect. attach(map, opts?) and inspectAll(map, point, opts?) need a GL JS map instance; inspect(feature, opts?) decodes locally but checks entitlement over HTTP first.

chrt.tiles.groups() — deprecated

Tilesets were consolidated to one source per region (2026-04). The route no longer exists — calling it fails with ChrtError 404. Use chrt.style() or chrt.styleUrl().

Error Handling

import { Chrt, ChrtError } from '@chrtco/sdk';

try {
  await chrt.features({ bbox: [-71, 42, -70, 43] });
} catch (e) {
  if (e instanceof ChrtError) {
    console.error(e.status, e.code, e.message);
  }
}

Typed errors by area: ChrtError (REST), TileTokenError (minting — invalid_api_key, region_not_licensed, tile_quota_exceeded, region_not_available, feature_flag_disabled, network_error, unexpected_response), InteractError (interact_not_enabled — includes the current plan and an upgrade hint — invalid_api_key, missing_scope, enrich_failed, unknown_layer, network_error, unexpected_response), and OfflineError (below).

Offline Downloads (CHRToffline)

Download MBTiles bundles for offline rendering. Bundles are SHA-256 verified before commit. Interrupted downloads resume from a .partial sidecar file.

Requires the Pro plan or above, enforced server-side at mint time — Free and Dev keys get OfflineError code offline_requires_pro_plan. The key also needs the offline:download scope, which is checked before the plan: a key without it gets offline_not_enabled regardless of tier, so add the scope first or the plan refusal never surfaces.

Runtime support in v1: Node 18+, Bun, Deno. Browser and React Native are deferred to a follow-up.

import { Chrt } from '@chrtco/sdk';
import { nodeFileSystemAdapter } from '@chrtco/sdk/node';

const chrt = new Chrt({
  apiKey: process.env.CHRT_API_KEY!,
  // Required for offline.* — generate once at first launch, persist per
  // install. Echoed into the offline JWT + heartbeat for device-level usage
  // tracking.
  deviceId: 'my-app-install-uuid',
  // Optional — enables budget enforcement + `list()` of local state. If
  // unset, budget is not enforced and `setStorageBudget()` throws.
  manifestPath: './chrt-offline-manifest.json',
});

const fs = nodeFileSystemAdapter();

// Optional: cap total local storage across all regions.
await chrt.offline.setStorageBudget(10 * 1024 ** 3, fs); // 10 GB

// List what you're licensed to download.
const regions = await chrt.offline.list();

// Download a region. Resumes from a partial if one exists.
await chrt.offline.download('NOAA_ENC', {
  path: './noaa.mbtiles',
  fs,
  onProgress: (p) => console.log(`${p.fraction * 100}%`),
});

// Remove a region (deletes the file + updates manifest + heartbeats).
await chrt.offline.remove('NOAA_ENC', { path: './noaa.mbtiles', fs });

// Manual heartbeat — call at app launch + every 24h while running.
await chrt.offline.heartbeat({ fs }); // derives regions + total from manifest

Typed offline errors

import { OfflineError } from '@chrtco/sdk';

try {
  await chrt.offline.download('NOAA_ENC', { path: './noaa.mbtiles', fs });
} catch (e) {
  if (e instanceof OfflineError) {
    switch (e.code) {
      case 'budget_exceeded': console.log(e.detail); break;
      case 'download_corrupt': console.log('SHA mismatch'); break;
      case 'network_interrupted': console.log('resume later'); break;
      case 'coverage_denied': console.log('region not licensed'); break;
      case 'offline_requires_pro_plan': console.log('CHRToffline needs Pro+ — chrt.co/pricing'); break;
      case 'offline_not_enabled': console.log('add the offline:download scope to this API key'); break;
    }
  }
}

Notes

  • The SDK mints its own short-lived JWTs internally. You never see them.
  • Heartbeats in v1 are manual — auto-scheduling comes in a later phase.
  • A budget smaller than current usage will not auto-prune; it only refuses new downloads. Use remove() explicitly.

License

MIT