@chrtco/sdk 0.4.0 → 0.5.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.d.ts CHANGED
@@ -1,3 +1,108 @@
1
+ import { F as FileSystemAdapter } from './fs-adapter-D5iwdb1Q.js';
2
+
3
+ /**
4
+ * Pure helper: take an online MapLibre style and rewrite the named region's
5
+ * vector source to point at a local MBTiles file. Layers, paint, layout,
6
+ * glyphs, and sprite are untouched.
7
+ *
8
+ * This is the JS counterpart of the offline-worker's server-side rewriter
9
+ * (see `cloudflare/offline-worker/src/common.ts:rewriteStyleSources`).
10
+ * Customers can either:
11
+ *
12
+ * 1. Fetch a pre-baked offline style from the worker via
13
+ * `chrt.offline.fetchStyle(region, opts)` — recommended for native
14
+ * MapLibre clients (iOS/Android/desktop) that don't have the JS atlas
15
+ * available.
16
+ *
17
+ * 2. Generate the online style themselves (e.g. via the React app's
18
+ * `src/lib/atlas/style-generator.ts`) and call `buildOfflineStyle()`
19
+ * locally — recommended for JS/web SDK consumers who already use the
20
+ * atlas at runtime.
21
+ *
22
+ * Both paths produce equivalent output.
23
+ */
24
+ /**
25
+ * MapLibre style specification (subset). Matches the JSON shape MapLibre GL
26
+ * JS, MapLibre Native iOS/Android, and the MapLibre style spec all consume.
27
+ * Customers who want full TypeScript style typing should reference
28
+ * `@maplibre/maplibre-gl-style-spec.StyleSpecification` directly.
29
+ */
30
+ interface MapLibreStyle {
31
+ version: number;
32
+ name?: string;
33
+ metadata?: Record<string, unknown>;
34
+ sources: Record<string, MapLibreSource | unknown>;
35
+ sprite?: string;
36
+ glyphs?: string;
37
+ layers: unknown[];
38
+ }
39
+ interface MapLibreSource {
40
+ type: string;
41
+ tiles?: string[];
42
+ url?: string;
43
+ minzoom?: number;
44
+ maxzoom?: number;
45
+ [key: string]: unknown;
46
+ }
47
+ /** Target platform — drives the URL scheme used in the rewritten source. */
48
+ type OfflineStylePlatform = "ios" | "android" | "desktop";
49
+ interface BuildOfflineStyleOptions {
50
+ /**
51
+ * Target platform. iOS + Android use the `mbtiles://` scheme MapLibre Native
52
+ * understands; desktop uses `file://`. Default: `"ios"`.
53
+ */
54
+ platform?: OfflineStylePlatform;
55
+ /**
56
+ * Name of the source to rewrite. If omitted, defaults to the only vector
57
+ * source in the input style. Throws `OfflineStyleError` if there are zero
58
+ * or more than one vector sources and `sourceName` isn't provided.
59
+ */
60
+ sourceName?: string;
61
+ }
62
+ /**
63
+ * Thrown when `buildOfflineStyle` can't determine which source to swap or
64
+ * the named source isn't present in the input style.
65
+ */
66
+ declare class OfflineStyleError extends Error {
67
+ readonly code: string;
68
+ constructor(message: string, code: string);
69
+ }
70
+ /**
71
+ * Returns a deep-copy of `onlineStyle` with the named (or auto-detected)
72
+ * vector source rewritten to point at `mbtilesPath`. Other sources, layers,
73
+ * paint, layout, glyphs, and sprite pass through unmodified.
74
+ *
75
+ * URL scheme:
76
+ *
77
+ * platform="ios" → `mbtiles://<mbtilesPath>` MLN Native iOS
78
+ * platform="android" → `mbtiles://<mbtilesPath>` MLN Native Android
79
+ * platform="desktop" → `file://<mbtilesPath>` MLN GL native (Win/Mac/Linux)
80
+ *
81
+ * The `mbtilesPath` is intentionally treated as opaque text. Most native MLN
82
+ * bindings accept the bare scheme + filename and resolve against an
83
+ * app-defined root (typically the app's Documents directory). Pass an
84
+ * absolute path on desktop, or a relative filename on iOS/Android.
85
+ *
86
+ * @param onlineStyle The base MapLibre style. Not mutated.
87
+ * @param mbtilesPath Local-file path component (e.g. `"NOAA_ENC.mbtiles"`).
88
+ * @param opts Platform + optional explicit `sourceName`.
89
+ *
90
+ * @example
91
+ * import { buildOfflineStyle } from "@chrtco/sdk";
92
+ *
93
+ * // Online style obtained however you like — generated client-side or
94
+ * // fetched from a server.
95
+ * const onlineStyle = generateStyle("DAY", { region: "NOAA_ENC" });
96
+ *
97
+ * const offlineStyle = buildOfflineStyle(
98
+ * onlineStyle,
99
+ * "NOAA_ENC.mbtiles",
100
+ * { platform: "ios" },
101
+ * );
102
+ * // → hand `offlineStyle` to MapLibre.
103
+ */
104
+ declare function buildOfflineStyle(onlineStyle: MapLibreStyle, mbtilesPath: string, opts?: BuildOfflineStyleOptions): MapLibreStyle;
105
+
1
106
  interface GeoJsonGeometry {
2
107
  type: string;
3
108
  coordinates: unknown;
@@ -68,6 +173,48 @@ interface CellsResponse {
68
173
  limit: number;
69
174
  offset: number;
70
175
  }
176
+ interface RouteQueryOptions {
177
+ /** Array of [longitude, latitude] waypoints forming the route */
178
+ route: [number, number][];
179
+ /** Buffer distance in metres (default 1000, max 50000) */
180
+ buffer?: number;
181
+ /** Filter by chart source, e.g. "NOAA" */
182
+ source?: string;
183
+ /** Filter by scale band, e.g. "4" */
184
+ scaleBand?: string;
185
+ /** Max cells to return (default 100) */
186
+ limit?: number;
187
+ /** AbortSignal to cancel the request */
188
+ signal?: AbortSignal;
189
+ }
190
+ interface RouteQueryResponse {
191
+ cells: CellDetail[];
192
+ query: {
193
+ buffer_m: number;
194
+ cell_count: number;
195
+ };
196
+ total_count: number;
197
+ }
198
+ interface RouteFeatureQueryOptions {
199
+ /** Array of [longitude, latitude] waypoints forming the route */
200
+ route: [number, number][];
201
+ /** Buffer distance in metres (default 1000, max 50000) */
202
+ buffer?: number;
203
+ /** Comma-separated layer codes to filter */
204
+ layer?: string;
205
+ /** Max features to return (default 100) */
206
+ limit?: number;
207
+ /** Pagination offset */
208
+ offset?: number;
209
+ /** AbortSignal to cancel the request */
210
+ signal?: AbortSignal;
211
+ }
212
+ interface RouteFeatureQueryResponse extends FeatureCollection {
213
+ query: {
214
+ buffer_m: number;
215
+ route_length_m?: number;
216
+ };
217
+ }
71
218
  interface CoverageSource {
72
219
  /** Chart source ID, e.g. "NOAA" */
73
220
  source: string;
@@ -117,6 +264,119 @@ interface ChrtConfig {
117
264
  baseUrl?: string;
118
265
  /** Max retries on 5xx/network errors (default 2) */
119
266
  retries?: number;
267
+ /**
268
+ * Stable per-install device identifier. Required to call `chrt.offline.*`.
269
+ * Generate once at first launch (e.g. UUIDv4), persist, reuse forever.
270
+ * Echoed into the offline JWT + heartbeat for device-level correlation.
271
+ */
272
+ deviceId?: string;
273
+ /**
274
+ * Absolute path to a JSON file where the SDK tracks local offline state
275
+ * (which regions are downloaded, their sizes and hashes, the storage
276
+ * budget). Optional — if unset, `setStorageBudget` throws and
277
+ * budget enforcement in `download` is skipped. No hidden state otherwise.
278
+ */
279
+ manifestPath?: string;
280
+ }
281
+ interface OfflineRegionSummary {
282
+ region: string;
283
+ build_id: string;
284
+ build_tag: string;
285
+ total_bytes: number;
286
+ issued_at: number | null;
287
+ }
288
+ /**
289
+ * Response shape from `GET /v1/offline/bundles/{region}`.
290
+ *
291
+ * Designed against the post-groups-collapse single-URL shape. A parallel task
292
+ * is migrating the offline-worker from `groups[]` → one URL per region; this
293
+ * SDK expects that migration to land before end-to-end integration.
294
+ */
295
+ interface OfflineBundleResponse {
296
+ region: string;
297
+ build_id: string;
298
+ build_tag: string;
299
+ signed_url: string;
300
+ expires_at: number;
301
+ bytes: number;
302
+ sha256: string;
303
+ offline_until: number;
304
+ heartbeat_after: number;
305
+ server_time: number;
306
+ }
307
+ interface OfflineDownloadProgress {
308
+ bytesDownloaded: number;
309
+ bytesTotal: number;
310
+ fraction: number;
311
+ }
312
+ interface OfflineDownloadOptions {
313
+ /** Final destination path for the downloaded MBTiles. SDK writes to `<path>.partial` first, then renames. */
314
+ path: string;
315
+ /** FileSystem adapter (e.g. `nodeFileSystemAdapter()` from `@chrtco/sdk/node`). */
316
+ fs: FileSystemAdapter;
317
+ /**
318
+ * Progress callback. Throttled to max(1 MB, 1%) boundaries. Always fires
319
+ * once at 0 bytes and once at completion regardless of threshold.
320
+ */
321
+ onProgress?: (p: OfflineDownloadProgress) => void;
322
+ /** AbortSignal — on abort, any bytes written persist as `<path>.partial` for resume. */
323
+ signal?: AbortSignal;
324
+ }
325
+ interface OfflineDownloadResult {
326
+ path: string;
327
+ bytes: number;
328
+ sha256: string;
329
+ build_id: string;
330
+ build_tag: string;
331
+ /** True if this call finished an in-flight download rather than starting fresh. */
332
+ resumed: boolean;
333
+ }
334
+ interface OfflineRemoveOptions {
335
+ path: string;
336
+ fs: FileSystemAdapter;
337
+ }
338
+ interface OfflineHeartbeatOptions {
339
+ /** Regions currently downloaded. If omitted, derived from manifest (requires `fs`). */
340
+ regions?: string[];
341
+ /** Total bytes on device. If omitted, derived from manifest (requires `fs`). */
342
+ total_bytes?: number;
343
+ /** FileSystem adapter. Required iff `regions`/`total_bytes` are omitted and manifestPath is set. */
344
+ fs?: FileSystemAdapter;
345
+ sdk_version?: string;
346
+ platform?: string;
347
+ }
348
+ /**
349
+ * Options for `chrt.offline.fetchStyle()` — fetch a pre-baked offline style
350
+ * from `GET /v1/offline/styles/{region}`.
351
+ */
352
+ interface OfflineFetchStyleOptions {
353
+ /** S-52 colour palette. Default: `"DAY"`. */
354
+ palette?: "DAY" | "DUSK" | "NIGHT";
355
+ /**
356
+ * URL scheme baked into the source. Default: `"mbtiles"` (iOS/Android via
357
+ * MLN Native). Use `"file"` for desktop (Win/macOS/Linux).
358
+ */
359
+ protocol?: "mbtiles" | "file";
360
+ /** AbortSignal to cancel the request. */
361
+ signal?: AbortSignal;
362
+ }
363
+ /**
364
+ * Response shape from `GET /v1/offline/styles/{region}`. The `style` field is
365
+ * a MapLibre style spec ready to hand to MLN; the surrounding metadata mirrors
366
+ * the bundle response so callers can refresh `offline_until` from the same
367
+ * call without a separate heartbeat.
368
+ */
369
+ interface OfflineStyleResponse {
370
+ region: string;
371
+ palette: "DAY" | "DUSK" | "NIGHT";
372
+ protocol: "mbtiles" | "file";
373
+ build_id: string;
374
+ build_tag: string;
375
+ /** Full MapLibre style JSON. Pass directly to `Map({ style: ... })`. */
376
+ style: MapLibreStyle;
377
+ offline_until: number;
378
+ heartbeat_after: number;
379
+ server_time: number;
120
380
  }
121
381
 
122
382
  declare class ChrtError extends Error {
@@ -133,6 +393,27 @@ declare class HttpClient {
133
393
  getBaseUrl(): string;
134
394
  getApiKey(): string;
135
395
  request<T>(path: string, params?: Record<string, string | number | undefined>, signal?: AbortSignal): Promise<T>;
396
+ /**
397
+ * Typed wrapper for non-GET JSON requests. Returns the parsed body and HTTP
398
+ * status together so callers (e.g. offline endpoints) can map error bodies
399
+ * to typed errors without throwing at the HTTP layer. Auth defaults to
400
+ * `x-api-key` header; pass `bearerToken` to swap to `Authorization: Bearer`.
401
+ * No retry — the offline flow owns its own retry logic.
402
+ */
403
+ postJson<T>(path: string, body: unknown, opts?: {
404
+ bearerToken?: string;
405
+ signal?: AbortSignal;
406
+ }): Promise<{
407
+ ok: boolean;
408
+ status: number;
409
+ body: T | null;
410
+ }>;
411
+ /** GET with Bearer auth (for offline-worker endpoints that require a JWT). */
412
+ getJsonWithBearer<T>(path: string, bearerToken: string, signal?: AbortSignal): Promise<{
413
+ ok: boolean;
414
+ status: number;
415
+ body: T | null;
416
+ }>;
136
417
  }
137
418
 
138
419
  declare class TilesEndpoint {
@@ -162,6 +443,100 @@ declare class CoverageEndpoint {
162
443
  list(options?: CoverageOptions): Promise<CoverageResponse>;
163
444
  }
164
445
 
446
+ /**
447
+ * Fresh-token shape from POST `/v1/mint/offline-token` (proxied to Supabase
448
+ * `mint-offline-token`). Mirrors docs/api/tile-jwt-claims.md §4.
449
+ */
450
+ interface MintedOfflineToken {
451
+ token: string;
452
+ expiresAt: number;
453
+ kid: string;
454
+ offlineUntil: number;
455
+ /** Per-region build_tag map returned by the mint function. */
456
+ buildTags: Record<string, string>;
457
+ gracePeriodDays: number;
458
+ }
459
+ /**
460
+ * Cache + single-flight mint for offline JWTs.
461
+ *
462
+ * - Keyed by `(deviceId, sorted-regions)` so a token scoped to one region
463
+ * doesn't accidentally get reused for a broader request.
464
+ * - Refreshes 30s before `exp`.
465
+ * - While a mint is in flight, concurrent callers share the same promise.
466
+ */
467
+ declare class OfflineTokenManager {
468
+ private readonly http;
469
+ private readonly cache;
470
+ private readonly pending;
471
+ constructor(http: HttpClient);
472
+ /**
473
+ * Get a fresh-enough token for the caller's device + requested regions.
474
+ * If `regions` is omitted, the mint function defaults to the key's full
475
+ * `allowed_sources`.
476
+ */
477
+ getToken(deviceId: string, regions?: string[]): Promise<MintedOfflineToken>;
478
+ /** Force-evict the cached token for a (deviceId, regions) pair. Used after a 401. */
479
+ invalidate(deviceId: string, regions?: string[]): void;
480
+ private cacheKey;
481
+ private mintFresh;
482
+ }
483
+
484
+ declare class OfflineEndpoint {
485
+ private readonly http;
486
+ private readonly deviceId;
487
+ private readonly manifestPath;
488
+ readonly tokenManager: OfflineTokenManager;
489
+ constructor(http: HttpClient, deviceId: string | undefined, manifestPath: string | undefined);
490
+ /** List regions downloadable under the caller's API key. */
491
+ list(): Promise<OfflineRegionSummary[]>;
492
+ /**
493
+ * Download an MBTiles bundle for `region` to `opts.path`. Resumable via a
494
+ * `<path>.partial` sidecar on interrupt. SHA-256 verified before rename.
495
+ */
496
+ download(region: string, opts: OfflineDownloadOptions): Promise<OfflineDownloadResult>;
497
+ /** Delete local file and, if manifest is configured, drop its row + heartbeat. */
498
+ remove(region: string, opts: OfflineRemoveOptions): Promise<void>;
499
+ /**
500
+ * Pure helper — rewrites the named (or auto-detected) vector source in
501
+ * `onlineStyle` to point at a local MBTiles file. Layers/paint/glyphs
502
+ * untouched. Use when you already have an online style (e.g. generated
503
+ * client-side via the React app's atlas) and want the offline variant.
504
+ *
505
+ * Equivalent to importing `buildOfflineStyle` directly — exposed on the
506
+ * endpoint for discoverability.
507
+ */
508
+ style(onlineStyle: MapLibreStyle, mbtilesPath: string, opts?: BuildOfflineStyleOptions): MapLibreStyle;
509
+ /**
510
+ * Fetch a pre-baked offline style for `region` from
511
+ * `GET /v1/offline/styles/{region}`. The worker's response already has the
512
+ * source rewritten to `mbtiles://` (default) or `file://`, so the returned
513
+ * `style` is ready to hand directly to MapLibre.
514
+ *
515
+ * Recommended for native MLN clients (iOS/Android/desktop) that don't have
516
+ * the JS atlas available. JS/web SDK consumers can equivalently generate
517
+ * the online style themselves and call `chrt.offline.style(...)`.
518
+ */
519
+ fetchStyle(region: string, opts?: OfflineFetchStyleOptions): Promise<OfflineStyleResponse>;
520
+ /**
521
+ * Set the local storage budget in bytes. Requires `manifestPath`. Enforced
522
+ * at the start of each `download()`. A budget smaller than current usage
523
+ * does not auto-prune; it only refuses new downloads until the customer
524
+ * calls `remove()`.
525
+ */
526
+ setStorageBudget(bytes: number, fs: FileSystemAdapter): Promise<void>;
527
+ /**
528
+ * Send a heartbeat. If `opts.regions` / `opts.total_bytes` are omitted and
529
+ * a manifest is configured, values are derived from it. v1 is manual-call
530
+ * only; auto-scheduling is a follow-up phase.
531
+ */
532
+ heartbeat(opts?: OfflineHeartbeatOptions): Promise<void>;
533
+ private requireDeviceId;
534
+ private requireToken;
535
+ private fetchBundle;
536
+ private downloadInternal;
537
+ private persistManifestEntry;
538
+ }
539
+
165
540
  /**
166
541
  * Generic async auto-pagination generator.
167
542
  *
@@ -241,6 +616,22 @@ type WebhookEventType = WebhookEvent["type"];
241
616
  */
242
617
  declare function parseWebhook(body: unknown): WebhookEvent;
243
618
 
619
+ /**
620
+ * Typed errors surfaced by `chrt.offline.*`.
621
+ *
622
+ * Callers switch on `.code` — never parse `.message`. `.status` is set only
623
+ * when the error originated from an HTTP response. `.detail` carries
624
+ * machine-readable context for programmatic handling (byte math for budget
625
+ * violations, `resumable: true` for interrupts, etc.).
626
+ */
627
+ type OfflineErrorCode = "budget_exceeded" | "coverage_denied" | "quota_exceeded" | "download_corrupt" | "network_interrupted" | "partial_file_invalid" | "invalid_api_key" | "region_not_available" | "offline_not_enabled" | "missing_device_id" | "missing_manifest_path" | "sha_missing" | "unexpected_response";
628
+ declare class OfflineError extends Error {
629
+ readonly code: OfflineErrorCode;
630
+ readonly status?: number;
631
+ readonly detail?: Record<string, unknown>;
632
+ constructor(code: OfflineErrorCode, message: string, status?: number, detail?: Record<string, unknown>);
633
+ }
634
+
244
635
  declare class Chrt {
245
636
  /** Tile endpoints — styleUrl, tileUrl, groups */
246
637
  readonly tiles: TilesEndpoint;
@@ -248,6 +639,12 @@ declare class Chrt {
248
639
  readonly cells: CellsEndpoint;
249
640
  /** Coverage data — list available chart sources */
250
641
  readonly coverage: CoverageEndpoint;
642
+ /**
643
+ * CHRToffline — download MBTiles bundles for offline use.
644
+ * Requires `deviceId` in config. Manifest-based budget enforcement
645
+ * requires `manifestPath`.
646
+ */
647
+ readonly offline: OfflineEndpoint;
251
648
  private featuresEp;
252
649
  private queryEp;
253
650
  private http;
@@ -293,4 +690,4 @@ declare class Chrt {
293
690
  transformRequest(): (url: string) => TransformRequest | undefined;
294
691
  }
295
692
 
296
- export { type CellDeletedEvent, type CellDetail, type CellEventData, type CellNewEvent, type CellUpdatedData, type CellUpdatedEvent, type CellsOptions, type CellsResponse, Chrt, type ChrtConfig, ChrtError, type CoverageOptions, type CoverageResponse, type CoverageSource, type FeatureCollection, type FeaturesOptions, type GeoJsonFeature, type GeoJsonGeometry, type QueryOptions, type TileGroup, type TileGroupsResponse, type TransformRequest, type WebhookEvent, type WebhookEventType, type WebhookPayload, paginate, parseWebhook };
693
+ export { type BuildOfflineStyleOptions, type CellDeletedEvent, type CellDetail, type CellEventData, type CellNewEvent, type CellUpdatedData, type CellUpdatedEvent, type CellsOptions, type CellsResponse, Chrt, type ChrtConfig, ChrtError, type CoverageOptions, type CoverageResponse, type CoverageSource, type FeatureCollection, type FeaturesOptions, FileSystemAdapter, type GeoJsonFeature, type GeoJsonGeometry, type MapLibreStyle, type OfflineBundleResponse, type OfflineDownloadOptions, type OfflineDownloadProgress, type OfflineDownloadResult, OfflineError, type OfflineErrorCode, type OfflineFetchStyleOptions, type OfflineHeartbeatOptions, type OfflineRegionSummary, type OfflineRemoveOptions, OfflineStyleError, type OfflineStylePlatform, type OfflineStyleResponse, type QueryOptions, type RouteFeatureQueryOptions, type RouteFeatureQueryResponse, type RouteQueryOptions, type RouteQueryResponse, type TileGroup, type TileGroupsResponse, type TransformRequest, type WebhookEvent, type WebhookEventType, type WebhookPayload, buildOfflineStyle, paginate, parseWebhook };