@chrtco/sdk 0.7.6 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ __export(index_exports, {
31
31
  ChrtError: () => ChrtError,
32
32
  DEFAULT_REGION: () => DEFAULT_REGION,
33
33
  DEFAULT_TILE_BASE_URL: () => DEFAULT_TILE_BASE_URL,
34
+ DEFAULT_TILE_REGION: () => DEFAULT_TILE_REGION,
34
35
  LABELED_LAYERS: () => LABELED_LAYERS,
35
36
  LABEL_BEHAVIOR: () => LABEL_BEHAVIOR,
36
37
  NAUTICAL_SYMBOLS: () => NAUTICAL_SYMBOLS,
@@ -48,6 +49,8 @@ __export(index_exports, {
48
49
  S52_NIGHT: () => S52_NIGHT,
49
50
  SYMBOL_GROUPS: () => SYMBOL_GROUPS,
50
51
  SYMBOL_MAP: () => SYMBOL_MAP,
52
+ TileAuth: () => TileAuth,
53
+ TileTokenError: () => TileTokenError,
51
54
  applyColorTheme: () => applyColorTheme,
52
55
  bandFilterExpression: () => bandFilterExpression,
53
56
  buildLabelLayer: () => buildLabelLayer,
@@ -226,20 +229,243 @@ function sleep(ms) {
226
229
  return new Promise((r) => setTimeout(r, ms));
227
230
  }
228
231
 
232
+ // src/tile-auth.ts
233
+ var DEFAULT_TILE_REGION = "NOAA_ENC";
234
+ var DEFAULT_TILE_BASE_URL = "https://tiles.chrt.co";
235
+ var TileTokenError = class extends Error {
236
+ code;
237
+ status;
238
+ constructor(code, message, status) {
239
+ super(message);
240
+ this.name = "TileTokenError";
241
+ this.code = code;
242
+ this.status = status;
243
+ }
244
+ };
245
+ var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.mvt(?:\?.*)?$/i;
246
+ var REFRESH_MARGIN_SECONDS = 30;
247
+ var STALE_GRACE_SECONDS = 1;
248
+ var MINT_RETRY_DELAYS_MS = [500, 1e3, 2e3];
249
+ function nowSeconds() {
250
+ return Math.floor(Date.now() / 1e3);
251
+ }
252
+ function sleep2(ms) {
253
+ return new Promise((r) => setTimeout(r, ms));
254
+ }
255
+ function mapErrorCode(raw, status) {
256
+ const known = [
257
+ "invalid_api_key",
258
+ "feature_flag_disabled",
259
+ "region_not_licensed",
260
+ "tile_quota_exceeded",
261
+ "region_not_available"
262
+ ];
263
+ if (typeof raw === "string" && known.includes(raw)) {
264
+ return raw;
265
+ }
266
+ if (status === 401) return "invalid_api_key";
267
+ if (status === 403) return "feature_flag_disabled";
268
+ if (status === 429) return "tile_quota_exceeded";
269
+ if (status === 503) return "region_not_available";
270
+ return "unexpected_response";
271
+ }
272
+ var TileAuth = class {
273
+ constructor(http, opts) {
274
+ this.http = http;
275
+ this.defaultRegion = opts?.region ?? DEFAULT_TILE_REGION;
276
+ this.tileBaseUrl = (opts?.tileBaseUrl ?? DEFAULT_TILE_BASE_URL).replace(/\/+$/, "");
277
+ }
278
+ cache = /* @__PURE__ */ new Map();
279
+ pending = /* @__PURE__ */ new Map();
280
+ defaultRegion;
281
+ tileBaseUrl;
282
+ lifecycleRegistered = false;
283
+ /** Mint once via the HTTP client (POST `/mint/tile-token`, `x-api-key`). */
284
+ async mintOnce(region) {
285
+ let res;
286
+ try {
287
+ res = await this.http.postJson("/mint/tile-token", { region });
288
+ } catch (err) {
289
+ throw new TileTokenError(
290
+ "network_error",
291
+ err instanceof Error ? err.message : "mint request failed"
292
+ );
293
+ }
294
+ if (!res.ok) {
295
+ const code = mapErrorCode(res.body?.error, res.status);
296
+ const msg = typeof res.body?.error === "string" ? res.body.error : `HTTP ${res.status}`;
297
+ throw new TileTokenError(code, msg, res.status);
298
+ }
299
+ const b = res.body;
300
+ if (!b || typeof b.token !== "string" || typeof b.expires_at !== "number" || typeof b.kid !== "string" || typeof b.build_tag !== "string" || typeof b.region !== "string") {
301
+ throw new TileTokenError(
302
+ "unexpected_response",
303
+ "mint-tile-token returned a malformed body",
304
+ res.status
305
+ );
306
+ }
307
+ return {
308
+ token: b.token,
309
+ expiresAt: b.expires_at,
310
+ kid: b.kid,
311
+ buildTag: b.build_tag,
312
+ region: b.region
313
+ };
314
+ }
315
+ /** Mint with exponential backoff on network errors only. */
316
+ async mintWithRetry(region) {
317
+ let lastErr;
318
+ for (let attempt = 0; attempt <= MINT_RETRY_DELAYS_MS.length; attempt++) {
319
+ try {
320
+ return await this.mintOnce(region);
321
+ } catch (err) {
322
+ lastErr = err;
323
+ if (err instanceof TileTokenError && err.code !== "network_error") throw err;
324
+ if (attempt === MINT_RETRY_DELAYS_MS.length) break;
325
+ await sleep2(MINT_RETRY_DELAYS_MS[attempt]);
326
+ }
327
+ }
328
+ throw lastErr instanceof Error ? lastErr : new TileTokenError("network_error", "mint failed");
329
+ }
330
+ scheduleRefresh(region, expiresAt) {
331
+ const delaySeconds = expiresAt - nowSeconds() - REFRESH_MARGIN_SECONDS;
332
+ if (delaySeconds <= 0) return null;
333
+ const timer = setTimeout(() => {
334
+ void this.refreshInBackground(region);
335
+ }, delaySeconds * 1e3);
336
+ timer.unref?.();
337
+ return timer;
338
+ }
339
+ async refreshInBackground(region) {
340
+ try {
341
+ const response = await this.mintWithRetry(region);
342
+ this.store(region, response);
343
+ } catch {
344
+ this.invalidate(region);
345
+ }
346
+ }
347
+ store(region, response) {
348
+ const existing = this.cache.get(region);
349
+ if (existing?.refreshTimer) clearTimeout(existing.refreshTimer);
350
+ this.cache.set(region, {
351
+ response,
352
+ refreshTimer: this.scheduleRefresh(region, response.expiresAt)
353
+ });
354
+ this.registerLifecycleHandlers();
355
+ }
356
+ /**
357
+ * Fetch (or reuse a cached) tile token for `region`. Concurrent callers for
358
+ * the same region await the same in-flight mint.
359
+ */
360
+ async getToken(region) {
361
+ const r = region ?? this.defaultRegion;
362
+ const cached = this.cache.get(r);
363
+ if (cached && cached.response.expiresAt - nowSeconds() > REFRESH_MARGIN_SECONDS) {
364
+ return cached.response;
365
+ }
366
+ const inflight = this.pending.get(r);
367
+ if (inflight) return inflight;
368
+ const promise = (async () => {
369
+ try {
370
+ const response = await this.mintWithRetry(r);
371
+ this.store(r, response);
372
+ return response;
373
+ } finally {
374
+ this.pending.delete(r);
375
+ }
376
+ })();
377
+ this.pending.set(r, promise);
378
+ return promise;
379
+ }
380
+ /** Synchronous cache peek — null if absent or within the stale grace window. */
381
+ peek(region) {
382
+ const entry = this.cache.get(region ?? this.defaultRegion);
383
+ if (!entry) return null;
384
+ if (entry.response.expiresAt - nowSeconds() <= STALE_GRACE_SECONDS) return null;
385
+ return entry.response.token;
386
+ }
387
+ /**
388
+ * Produce a MapLibre / Mapbox GL `transformRequest`. Attaches
389
+ * `Authorization: Bearer <jwt>` only to CHRT tile URLs; style, sprite, and
390
+ * glyph URLs pass through untouched (never leak the token to third-party
391
+ * CDNs). Kicks off a background mint immediately so the cache warms within
392
+ * one round-trip; the first tile or two may go unauthenticated and be
393
+ * retried by the renderer.
394
+ */
395
+ transformRequest(region) {
396
+ const r = region ?? this.defaultRegion;
397
+ void this.getToken(r).catch(() => void 0);
398
+ return (url) => {
399
+ if (!url.startsWith(this.tileBaseUrl) || !TILE_URL_PATH.test(url)) return void 0;
400
+ const token = this.peek(r);
401
+ if (!token) return { url };
402
+ return { url, headers: { Authorization: `Bearer ${token}` } };
403
+ };
404
+ }
405
+ /** Clear the cached token + timer for a region. */
406
+ invalidate(region) {
407
+ const r = region ?? this.defaultRegion;
408
+ const entry = this.cache.get(r);
409
+ if (entry?.refreshTimer) clearTimeout(entry.refreshTimer);
410
+ this.cache.delete(r);
411
+ }
412
+ /**
413
+ * Browser-only defense against `setTimeout` freezing while a tab is
414
+ * suspended (laptop sleep, mobile background, bfcache restore): proactively
415
+ * re-mint near-expiry entries on wake. Inert under SSR / Node / Deno.
416
+ */
417
+ registerLifecycleHandlers() {
418
+ if (this.lifecycleRegistered) return;
419
+ if (typeof document === "undefined" || typeof window === "undefined") return;
420
+ const onWake = () => {
421
+ if (document.visibilityState !== "visible") return;
422
+ for (const [region, entry] of this.cache.entries()) {
423
+ if (entry.response.expiresAt - nowSeconds() >= REFRESH_MARGIN_SECONDS) continue;
424
+ void this.refreshInBackground(region);
425
+ }
426
+ };
427
+ document.addEventListener("visibilitychange", onWake);
428
+ window.addEventListener("pageshow", (e) => {
429
+ if (e.persisted) onWake();
430
+ });
431
+ this.lifecycleRegistered = true;
432
+ }
433
+ };
434
+
229
435
  // src/endpoints/tiles.ts
230
436
  var TilesEndpoint = class {
231
- constructor(http) {
437
+ constructor(http, opts) {
232
438
  this.http = http;
439
+ this.region = opts?.region ?? DEFAULT_TILE_REGION;
440
+ this.tileBaseUrl = (opts?.tileBaseUrl ?? DEFAULT_TILE_BASE_URL).replace(/\/+$/, "");
233
441
  }
234
- /** Full style.json URL with API key as query param */
442
+ region;
443
+ tileBaseUrl;
444
+ /**
445
+ * @deprecated CHRT has no remote `style.json` endpoint — the S-52 style is
446
+ * generated client-side. Use `chrt.style(opts)` to get a ready-to-use
447
+ * MapLibre style object instead. Calling this throws to prevent silently
448
+ * pointing a map at a dead URL.
449
+ */
235
450
  styleUrl() {
236
- return `${this.http.getBaseUrl()}/tiles/style.json?key=${this.http.getApiKey()}`;
451
+ throw new Error(
452
+ "chrt.tiles.styleUrl() is no longer supported: CHRT serves no remote style.json. Use chrt.style(opts) to generate an S-52 MapLibre style object, and chrt.transformRequest() for tile auth. See https://chrt.co/docs/vector-tiles"
453
+ );
237
454
  }
238
- /** Individual tile URL template */
239
- tileUrl(z, x, y, format = "mvt") {
240
- return `${this.http.getBaseUrl()}/tiles/${z}/${x}/${y}.${format}?key=${this.http.getApiKey()}`;
455
+ /**
456
+ * Vector tile URL template for the configured region, served by the
457
+ * tile-worker at `tiles.chrt.co`. Tiles are JWT-gated — pair this with
458
+ * `chrt.transformRequest()`, which attaches the `Authorization: Bearer`
459
+ * header. Prefer `chrt.style()`, which wires the source for you.
460
+ */
461
+ tileUrl(z, x, y) {
462
+ return `${this.tileBaseUrl}/v1/tiles/${this.region}/${z}/${x}/${y}.mvt`;
241
463
  }
242
- /** List available tileset groups */
464
+ /**
465
+ * @deprecated Tilesets were consolidated to one PMTiles per region (2026-04);
466
+ * the per-group catalogue endpoint is legacy and no longer served. Render via
467
+ * `chrt.style()` instead, which emits a single vector source per region.
468
+ */
243
469
  async groups() {
244
470
  return this.http.request("/tiles/groups");
245
471
  }
@@ -431,7 +657,7 @@ function mapServerError(body, status) {
431
657
  }
432
658
 
433
659
  // src/endpoints/offline-token-manager.ts
434
- var REFRESH_MARGIN_SECONDS = 30;
660
+ var REFRESH_MARGIN_SECONDS2 = 30;
435
661
  function now() {
436
662
  return Math.floor(Date.now() / 1e3);
437
663
  }
@@ -452,7 +678,7 @@ var OfflineTokenManager = class {
452
678
  }
453
679
  const key = this.cacheKey(deviceId, regions);
454
680
  const cached = this.cache.get(key);
455
- if (cached && cached.expiresAt - now() > REFRESH_MARGIN_SECONDS) {
681
+ if (cached && cached.expiresAt - now() > REFRESH_MARGIN_SECONDS2) {
456
682
  return cached;
457
683
  }
458
684
  const inflight = this.pending.get(key);
@@ -1074,47 +1300,6 @@ function dirname(p) {
1074
1300
  return p.slice(0, k);
1075
1301
  }
1076
1302
 
1077
- // src/pagination.ts
1078
- async function* paginate(fetchPage, getItems, getTotalCount, options) {
1079
- const pageSize = options?.pageSize ?? 100;
1080
- const signal = options?.signal;
1081
- let offset = 0;
1082
- while (true) {
1083
- if (signal?.aborted) return;
1084
- const response = await fetchPage(offset, signal);
1085
- const items = getItems(response);
1086
- for (const item of items) {
1087
- yield item;
1088
- }
1089
- if (items.length === 0) return;
1090
- offset += items.length;
1091
- const total = getTotalCount(response);
1092
- if (total > 0 && offset >= total) return;
1093
- }
1094
- }
1095
-
1096
- // src/webhooks.ts
1097
- var VALID_TYPES = /* @__PURE__ */ new Set(["cell.updated", "cell.new", "cell.deleted"]);
1098
- function parseWebhook(body) {
1099
- if (typeof body !== "object" || body === null) {
1100
- throw new Error("Invalid webhook payload: expected an object");
1101
- }
1102
- const obj = body;
1103
- if (typeof obj.id !== "string") {
1104
- throw new Error("Invalid webhook payload: missing 'id'");
1105
- }
1106
- if (typeof obj.timestamp !== "string") {
1107
- throw new Error("Invalid webhook payload: missing 'timestamp'");
1108
- }
1109
- if (typeof obj.type !== "string" || !VALID_TYPES.has(obj.type)) {
1110
- throw new Error(`Invalid webhook payload: unknown type '${String(obj.type)}'`);
1111
- }
1112
- if (typeof obj.data !== "object" || obj.data === null) {
1113
- throw new Error("Invalid webhook payload: missing 'data'");
1114
- }
1115
- return obj;
1116
- }
1117
-
1118
1303
  // src/atlas/s52-colors.ts
1119
1304
  var S52_DAY = {
1120
1305
  // Depth
@@ -5315,7 +5500,10 @@ var NAUTICAL_SYMBOLS = [
5315
5500
  getSvg: landmarkSpire,
5316
5501
  group: "landmarks",
5317
5502
  description: "Church spire or steeple, conspicuous from seaward. Useful for visual fixing.",
5318
- s52PlRef: "LNDMRK17",
5503
+ // CATLMK 20 alone has no dedicated PL symbol (renders POSGEN01/03); church spires carry
5504
+ // FUNCTN 20/21 and render as the religious-building symbol BUIREL01 (BUIREL13 when conspicuous)
5505
+ s52PlRef: "BUIREL01",
5506
+ s57Class: "LNDMRK",
5319
5507
  attributeKey: "CATLMK",
5320
5508
  attributeValue: 20,
5321
5509
  renderSize: "32x32",
@@ -5332,9 +5520,11 @@ var NAUTICAL_SYMBOLS = [
5332
5520
  getSvg: landmarkTower,
5333
5521
  group: "landmarks",
5334
5522
  description: "Tower conspicuous from seaward. General-purpose landmark symbol.",
5335
- s52PlRef: "LNDMRK20",
5523
+ s52PlRef: "TOWERS01",
5524
+ s57Class: "LNDMRK",
5336
5525
  attributeKey: "CATLMK",
5337
5526
  attributeValue: 17,
5527
+ // TOWERS03 = conspicuous variant
5338
5528
  renderSize: "32x32",
5339
5529
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower body, cap, position circle" }],
5340
5530
  lineStyles: [{ element: "Tower outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5349,7 +5539,8 @@ var NAUTICAL_SYMBOLS = [
5349
5539
  getSvg: landmarkChimney,
5350
5540
  group: "landmarks",
5351
5541
  description: "Industrial chimney or smokestack visible from the sea.",
5352
- s52PlRef: "LNDMRK01",
5542
+ s52PlRef: "CHIMNY01",
5543
+ s57Class: "LNDMRK",
5353
5544
  attributeKey: "CATLMK",
5354
5545
  attributeValue: 3,
5355
5546
  renderSize: "32x32",
@@ -5366,7 +5557,8 @@ var NAUTICAL_SYMBOLS = [
5366
5557
  getSvg: landmarkFlagstaff,
5367
5558
  group: "landmarks",
5368
5559
  description: "Flagstaff or flagpole conspicuous from seaward.",
5369
- s52PlRef: "LNDMRK03",
5560
+ s52PlRef: "FLGSTF01",
5561
+ s57Class: "LNDMRK",
5370
5562
  attributeKey: "CATLMK",
5371
5563
  attributeValue: 5,
5372
5564
  renderSize: "32x32",
@@ -5386,9 +5578,11 @@ var NAUTICAL_SYMBOLS = [
5386
5578
  getSvg: landmarkMonument,
5387
5579
  group: "landmarks",
5388
5580
  description: "Monument or memorial conspicuous from the sea.",
5389
- s52PlRef: "LNDMRK05",
5581
+ s52PlRef: "MONUMT12",
5582
+ s57Class: "LNDMRK",
5390
5583
  attributeKey: "CATLMK",
5391
5584
  attributeValue: 9,
5585
+ // MONUMT02 = non-conspicuous variant
5392
5586
  renderSize: "32x32",
5393
5587
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Monument body, base, position circle" }],
5394
5588
  lineStyles: [{ element: "Body outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5404,6 +5598,7 @@ var NAUTICAL_SYMBOLS = [
5404
5598
  group: "landmarks",
5405
5599
  description: "Traditional windmill with rotating vanes. Conspicuous landmark.",
5406
5600
  s52PlRef: "WNDMIL12",
5601
+ s57Class: "LNDMRK",
5407
5602
  attributeKey: "CATLMK",
5408
5603
  attributeValue: 18,
5409
5604
  renderSize: "32x32",
@@ -5421,6 +5616,7 @@ var NAUTICAL_SYMBOLS = [
5421
5616
  group: "landmarks",
5422
5617
  description: "Modern 3-blade wind turbine. Increasingly common offshore landmark.",
5423
5618
  s52PlRef: "WIMCON11",
5619
+ s57Class: "LNDMRK",
5424
5620
  attributeKey: "CATLMK",
5425
5621
  attributeValue: 19,
5426
5622
  renderSize: "32x32",
@@ -5437,9 +5633,11 @@ var NAUTICAL_SYMBOLS = [
5437
5633
  getSvg: landmarkDome,
5438
5634
  group: "landmarks",
5439
5635
  description: "Dome or cupola (e.g., observatory, mosque) visible from the sea.",
5440
- s52PlRef: "LNDMRK03",
5636
+ s52PlRef: "DOMES011",
5637
+ s57Class: "LNDMRK",
5441
5638
  attributeKey: "CATLMK",
5442
5639
  attributeValue: 15,
5640
+ // DOMES001 = non-conspicuous variant
5443
5641
  renderSize: "32x32",
5444
5642
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Dome, base, position circle" }],
5445
5643
  lineStyles: [{ element: "Base outline", width: 0.8, color: "#000000" }, { element: "Dome curve", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5454,7 +5652,8 @@ var NAUTICAL_SYMBOLS = [
5454
5652
  getSvg: landmarkRadarScanner,
5455
5653
  group: "landmarks",
5456
5654
  description: "Radar scanner antenna conspicuous from seaward.",
5457
- s52PlRef: "LNDMRK12",
5655
+ s52PlRef: "RASCAN01",
5656
+ s57Class: "LNDMRK",
5458
5657
  attributeKey: "CATLMK",
5459
5658
  attributeValue: 16,
5460
5659
  renderSize: "32x32",
@@ -5471,7 +5670,8 @@ var NAUTICAL_SYMBOLS = [
5471
5670
  getSvg: landmarkDishAerial,
5472
5671
  group: "landmarks",
5473
5672
  description: "Satellite dish or parabolic antenna visible from the sea.",
5474
- s52PlRef: "LNDMRK12",
5673
+ s52PlRef: "DSHAER01",
5674
+ s57Class: "LNDMRK",
5475
5675
  attributeKey: "CATLMK",
5476
5676
  attributeValue: 4,
5477
5677
  renderSize: "32x32",
@@ -5489,6 +5689,7 @@ var NAUTICAL_SYMBOLS = [
5489
5689
  group: "landmarks",
5490
5690
  description: "Water tower. Elevated tank on supporting structure, conspicuous landmark.",
5491
5691
  s52PlRef: "TOWERS12",
5692
+ s57Class: "SILTNK",
5492
5693
  attributeKey: "CATSIL",
5493
5694
  attributeValue: 4,
5494
5695
  renderSize: "32x32",
@@ -5505,7 +5706,9 @@ var NAUTICAL_SYMBOLS = [
5505
5706
  getSvg: landmarkRadioTvMast,
5506
5707
  group: "landmarks",
5507
5708
  description: "Radio or television transmission mast with horizontal cross-arms.",
5508
- s52PlRef: "LNDMRK10",
5709
+ // Radio/TV masts are CATLMK 7 (mast) + FUNCTN in S-57, so both mast entries share MSTCON04
5710
+ s52PlRef: "MSTCON04",
5711
+ s57Class: "LNDMRK",
5509
5712
  attributeKey: "CATLMK",
5510
5713
  attributeValue: 7,
5511
5714
  renderSize: "32x32",
@@ -5522,7 +5725,10 @@ var NAUTICAL_SYMBOLS = [
5522
5725
  getSvg: landmarkCross,
5523
5726
  group: "landmarks",
5524
5727
  description: "Religious cross or calvary conspicuous from seaward.",
5525
- s52PlRef: "LNDMRK05",
5728
+ // The PL has no dedicated symbol for CATLMK 14 (cross/calvary) — S-52 falls back to the
5729
+ // generic position symbol POSGEN01 (POSGEN03 when conspicuous)
5730
+ s52PlRef: "POSGEN01",
5731
+ s57Class: "LNDMRK",
5526
5732
  attributeKey: "CATLMK",
5527
5733
  attributeValue: 14,
5528
5734
  renderSize: "32x32",
@@ -5539,9 +5745,11 @@ var NAUTICAL_SYMBOLS = [
5539
5745
  getSvg: landmarkMast,
5540
5746
  group: "landmarks",
5541
5747
  description: "General mast (not radio/TV). Pole with spherical top.",
5542
- s52PlRef: "LNDMRK10",
5748
+ s52PlRef: "MSTCON04",
5749
+ s57Class: "LNDMRK",
5543
5750
  attributeKey: "CATLMK",
5544
5751
  attributeValue: 7,
5752
+ // MSTCON14 = conspicuous variant
5545
5753
  renderSize: "32x32",
5546
5754
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Mast pole, top ball, position circle" }],
5547
5755
  lineStyles: [{ element: "Mast pole", width: 2, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5557,6 +5765,7 @@ var NAUTICAL_SYMBOLS = [
5557
5765
  group: "landmarks",
5558
5766
  description: "Silo, grain elevator, or storage tank conspicuous from the sea.",
5559
5767
  s52PlRef: "SILBUI01",
5768
+ s57Class: "SILTNK",
5560
5769
  attributeKey: "CATSIL",
5561
5770
  attributeValue: 1,
5562
5771
  renderSize: "32x32",
@@ -8267,6 +8476,18 @@ var S101_FEATURES = [
8267
8476
  isNewInS101: false,
8268
8477
  attributes: ["categoryOfLandmark", "function", "visuallyConspicuous"]
8269
8478
  },
8479
+ {
8480
+ featureClass: "SiloStorageTank",
8481
+ name: "Silo / Storage Tank",
8482
+ s57Equivalent: "SILTNK",
8483
+ section: "E",
8484
+ group: "landmarks",
8485
+ category: "geo",
8486
+ description: "An enclosed structure for storing bulk solids or liquids (silo, grain elevator, water tower, tank). Often conspicuous and useful for position fixing.",
8487
+ portrayalRef: "SILTNK_S101",
8488
+ isNewInS101: false,
8489
+ attributes: ["categoryOfSiloTank", "colour", "visuallyConspicuous"]
8490
+ },
8270
8491
  // ── Section D: Bridges ──
8271
8492
  {
8272
8493
  featureClass: "Bridge",
@@ -9443,7 +9664,7 @@ var BUILD_MINZOOM = 3;
9443
9664
  var BUILD_MAXZOOM = 14;
9444
9665
 
9445
9666
  // src/atlas/source-registry.ts
9446
- var DEFAULT_TILE_BASE_URL = "https://tiles.chrt.co";
9667
+ var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
9447
9668
  var DEFAULT_REGION = "NOAA_ENC";
9448
9669
  function buildSources(opts = {}) {
9449
9670
  const mode = opts.mode ?? "pmtiles";
@@ -9456,7 +9677,7 @@ function buildSources(opts = {}) {
9456
9677
  }
9457
9678
  };
9458
9679
  }
9459
- const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL).replace(/\/+$/, "");
9680
+ const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
9460
9681
  return {
9461
9682
  [region]: {
9462
9683
  type: "vector",
@@ -11183,6 +11404,47 @@ function getStyleStats(style) {
11183
11404
  };
11184
11405
  }
11185
11406
 
11407
+ // src/pagination.ts
11408
+ async function* paginate(fetchPage, getItems, getTotalCount, options) {
11409
+ const pageSize = options?.pageSize ?? 100;
11410
+ const signal = options?.signal;
11411
+ let offset = 0;
11412
+ while (true) {
11413
+ if (signal?.aborted) return;
11414
+ const response = await fetchPage(offset, signal);
11415
+ const items = getItems(response);
11416
+ for (const item of items) {
11417
+ yield item;
11418
+ }
11419
+ if (items.length === 0) return;
11420
+ offset += items.length;
11421
+ const total = getTotalCount(response);
11422
+ if (total > 0 && offset >= total) return;
11423
+ }
11424
+ }
11425
+
11426
+ // src/webhooks.ts
11427
+ var VALID_TYPES = /* @__PURE__ */ new Set(["cell.updated", "cell.new", "cell.deleted"]);
11428
+ function parseWebhook(body) {
11429
+ if (typeof body !== "object" || body === null) {
11430
+ throw new Error("Invalid webhook payload: expected an object");
11431
+ }
11432
+ const obj = body;
11433
+ if (typeof obj.id !== "string") {
11434
+ throw new Error("Invalid webhook payload: missing 'id'");
11435
+ }
11436
+ if (typeof obj.timestamp !== "string") {
11437
+ throw new Error("Invalid webhook payload: missing 'timestamp'");
11438
+ }
11439
+ if (typeof obj.type !== "string" || !VALID_TYPES.has(obj.type)) {
11440
+ throw new Error(`Invalid webhook payload: unknown type '${String(obj.type)}'`);
11441
+ }
11442
+ if (typeof obj.data !== "object" || obj.data === null) {
11443
+ throw new Error("Invalid webhook payload: missing 'data'");
11444
+ }
11445
+ return obj;
11446
+ }
11447
+
11186
11448
  // src/index.ts
11187
11449
  var Chrt = class {
11188
11450
  /** Tile endpoints — styleUrl, tileUrl, groups */
@@ -11200,15 +11462,22 @@ var Chrt = class {
11200
11462
  featuresEp;
11201
11463
  queryEp;
11202
11464
  http;
11465
+ tileAuth;
11466
+ region;
11467
+ tileBaseUrl;
11203
11468
  constructor(apiKeyOrConfig) {
11204
11469
  const config = typeof apiKeyOrConfig === "string" ? { apiKey: apiKeyOrConfig } : apiKeyOrConfig;
11470
+ this.region = config.region ?? DEFAULT_TILE_REGION;
11471
+ this.tileBaseUrl = config.tileBaseUrl ?? DEFAULT_TILE_BASE_URL;
11205
11472
  this.http = new HttpClient(config.apiKey, config.baseUrl, config.retries);
11206
- this.tiles = new TilesEndpoint(this.http);
11473
+ const tileOpts = { region: this.region, tileBaseUrl: this.tileBaseUrl };
11474
+ this.tiles = new TilesEndpoint(this.http, tileOpts);
11207
11475
  this.cells = new CellsEndpoint(this.http);
11208
11476
  this.coverage = new CoverageEndpoint(this.http);
11209
11477
  this.featuresEp = new FeaturesEndpoint(this.http);
11210
11478
  this.queryEp = new QueryEndpoint(this.http);
11211
11479
  this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
11480
+ this.tileAuth = new TileAuth(this.http, tileOpts);
11212
11481
  }
11213
11482
  /* ── Convenience shortcuts ───────────────────────── */
11214
11483
  /** Query GeoJSON features by bounding box */
@@ -11285,28 +11554,63 @@ var Chrt = class {
11285
11554
  );
11286
11555
  }
11287
11556
  /* ── Map integration helpers ─────────────────────── */
11288
- /** Returns a style.json URL ready for Mapbox GL / MapLibre */
11557
+ /**
11558
+ * Build a ready-to-use MapLibre / Mapbox GL style object for CHRT's nautical
11559
+ * charts. The default is the full IHO **S-52** portrayal; pick a palette and
11560
+ * tweak rendering via `opts`. The returned object is a standard MapLibre
11561
+ * style — mutate it freely, or build your own from scratch over the CHRT
11562
+ * vector source (see `buildSources`) for a fully custom look.
11563
+ *
11564
+ * Pair with `transformRequest()` so tile requests carry their auth token.
11565
+ *
11566
+ * @example
11567
+ * const map = new maplibregl.Map({
11568
+ * container: 'map',
11569
+ * style: chrt.style({ palette: 'DAY' }),
11570
+ * transformRequest: chrt.transformRequest(),
11571
+ * });
11572
+ *
11573
+ * @remarks S-101 portrayal is on the roadmap; today `style()` emits S-52.
11574
+ */
11575
+ style(opts = {}) {
11576
+ const { palette = "DAY", ...styleOpts } = opts;
11577
+ return generateStyle(palette, {
11578
+ region: this.region,
11579
+ tileBaseUrl: this.tileBaseUrl,
11580
+ ...styleOpts
11581
+ });
11582
+ }
11583
+ /**
11584
+ * @deprecated CHRT serves no remote `style.json`. Use {@link style} to get a
11585
+ * MapLibre style object instead. Throws to avoid silently pointing a map at a
11586
+ * dead URL.
11587
+ */
11289
11588
  styleUrl() {
11290
11589
  return this.tiles.styleUrl();
11291
11590
  }
11292
11591
  /**
11293
- * Returns a `transformRequest` function for Mapbox GL JS / MapLibre GL JS.
11294
- * Injects the API key header for requests to api.chrt.co.
11592
+ * Returns a `transformRequest` for Mapbox GL JS / MapLibre GL JS that mints a
11593
+ * short-lived tile JWT from your API key and attaches it as
11594
+ * `Authorization: Bearer` on requests to `tiles.chrt.co` only. Style, sprite,
11595
+ * and glyph URLs pass through untouched. The cache warms in the background on
11596
+ * first call, so the first tile or two may be retried by the renderer.
11295
11597
  *
11296
11598
  * @example
11297
- * const map = new mapboxgl.Map({
11298
- * style: chrt.styleUrl(),
11599
+ * const map = new maplibregl.Map({
11600
+ * style: chrt.style(),
11299
11601
  * transformRequest: chrt.transformRequest(),
11300
11602
  * });
11301
11603
  */
11302
- transformRequest() {
11303
- const apiKey = this.http.getApiKey();
11304
- return (url) => {
11305
- if (url.includes("api.chrt.co")) {
11306
- return { url, headers: { "x-api-key": apiKey } };
11307
- }
11308
- return void 0;
11309
- };
11604
+ transformRequest(opts) {
11605
+ return this.tileAuth.transformRequest(opts?.region);
11606
+ }
11607
+ /**
11608
+ * Pre-mint the tile token for a region. Optional — `transformRequest()` warms
11609
+ * the cache automatically — but useful if you want to surface a mint error
11610
+ * (e.g. `region_not_licensed`, `tile_quota_exceeded`) before the map loads.
11611
+ */
11612
+ async warmTiles(region) {
11613
+ await this.tileAuth.getToken(region);
11310
11614
  }
11311
11615
  };
11312
11616
  // Annotate the CommonJS export names for ESM import in node:
@@ -11322,6 +11626,7 @@ var Chrt = class {
11322
11626
  ChrtError,
11323
11627
  DEFAULT_REGION,
11324
11628
  DEFAULT_TILE_BASE_URL,
11629
+ DEFAULT_TILE_REGION,
11325
11630
  LABELED_LAYERS,
11326
11631
  LABEL_BEHAVIOR,
11327
11632
  NAUTICAL_SYMBOLS,
@@ -11339,6 +11644,8 @@ var Chrt = class {
11339
11644
  S52_NIGHT,
11340
11645
  SYMBOL_GROUPS,
11341
11646
  SYMBOL_MAP,
11647
+ TileAuth,
11648
+ TileTokenError,
11342
11649
  applyColorTheme,
11343
11650
  bandFilterExpression,
11344
11651
  buildLabelLayer,