@walkthru-earth/objex-utils 1.3.0 → 1.4.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.cts CHANGED
@@ -39,7 +39,7 @@ declare const COPY_FEEDBACK_MS = 2000;
39
39
  * Single source of truth for extension → icon, color, label, category,
40
40
  * viewer, DuckDB read function, MIME type, and queryability.
41
41
  *
42
- * Used by: FileRow, FileTreeSidebar, ViewerRouter, TableViewer, WasmQueryEngine, etc.
42
+ * Used by: FileTreeSidebar, ViewerRouter, TableViewer, WasmQueryEngine, etc.
43
43
  */
44
44
  type FileCategory = 'data' | 'geo' | 'code' | 'document' | 'config' | 'image' | 'video' | 'audio' | 'archive' | 'database' | '3d' | 'other';
45
45
  type ViewerKind = 'table' | 'image' | 'video' | 'audio' | 'markdown' | 'code' | 'cog' | 'pmtiles' | 'flatgeobuf' | 'pdf' | '3d' | 'archive' | 'database' | 'zarr' | 'copc' | 'notebook' | 'raw';
@@ -149,6 +149,16 @@ interface QueryEngine {
149
149
  crs: string | null;
150
150
  }>;
151
151
  queryCancellable?(connId: string, sql: string): QueryHandle;
152
+ /**
153
+ * Streaming variant of `queryCancellable`. Yields one `QueryResult`-shaped
154
+ * chunk per Arrow RecordBatch instead of materializing the full row set
155
+ * before returning. Peak memory tracks one batch (~64 KiB rows) rather
156
+ * than the full result, which avoids DuckDB-WASM OOMs on large parquet
157
+ * scans. The first chunk carries the schema (`columns` / `types`); later
158
+ * chunks repeat them so consumers don't need to track first-batch state.
159
+ * Cancellation aborts the inner DuckDB cursor.
160
+ */
161
+ queryStream?(connId: string, sql: string, signal?: AbortSignal): AsyncIterable<QueryResult>;
152
162
  queryForMapCancellable?(connId: string, sql: string, geomCol: string, geomColType: string, sourceCrs?: string | null): MapQueryHandle;
153
163
  forceCancel?(): Promise<void>;
154
164
  /** Register a file buffer in DuckDB-WASM's virtual filesystem for ATTACH. */
@@ -344,6 +354,431 @@ declare class UrlAdapter implements StorageAdapter {
344
354
  copy(): Promise<WriteResult>;
345
355
  }
346
356
 
357
+ /** A basemap option a host can offer. Consumed in Phase 2. */
358
+ interface BasemapConfig {
359
+ id: string;
360
+ label: string;
361
+ type: 'vector' | 'raster';
362
+ url: string;
363
+ variant?: 'light' | 'dark';
364
+ }
365
+ /** A preloaded connection definition. Never carries secrets. Consumed in Phase 2. */
366
+ interface ConnectionSeed {
367
+ name: string;
368
+ provider: string;
369
+ bucket: string;
370
+ region?: string;
371
+ endpoint?: string;
372
+ anonymous?: boolean;
373
+ authMethod?: 'sigv4' | 'sas-token';
374
+ rootPrefix?: string;
375
+ }
376
+ interface AppConfigDefaults {
377
+ theme: Theme;
378
+ locale: string;
379
+ featureLimit: number;
380
+ mosaicItemLimit: number;
381
+ }
382
+ interface AppConfigUi {
383
+ showConnectionRail: boolean;
384
+ showFileTree: boolean;
385
+ showSettings: boolean;
386
+ }
387
+ interface AppConfig {
388
+ defaults: AppConfigDefaults;
389
+ ui: AppConfigUi;
390
+ basemaps: BasemapConfig[];
391
+ defaultBasemap: {
392
+ light?: string;
393
+ dark?: string;
394
+ };
395
+ connections: ConnectionSeed[];
396
+ }
397
+ /** Hardcoded fallback. Matches current app behaviour when no config is present. */
398
+ declare const DEFAULT_APP_CONFIG: AppConfig;
399
+ declare function coerceTheme(v: unknown): Theme | undefined;
400
+ declare function coerceString(v: unknown): string | undefined;
401
+ declare function coercePositiveInt(v: unknown): number | undefined;
402
+ declare function coerceBool(v: unknown): boolean | undefined;
403
+ /** First defined (non-null, non-undefined) candidate wins. Encodes the precedence chain. */
404
+ declare function resolveSetting<T>(...candidates: (T | null | undefined)[]): T | undefined;
405
+ /** Maps the ?sidebar / ?tree visibility param to a boolean, or undefined when absent or invalid. */
406
+ declare function parseVisibilityParam(value: string | null): boolean | undefined;
407
+ /**
408
+ * Pick the basemap a map should render. Precedence: explicit user pick (when it
409
+ * still exists in the configured list) > the configured default for the theme
410
+ * variant > the first basemap matching the variant > the first basemap of any
411
+ * variant. Returns undefined when no basemaps are configured, signalling the
412
+ * caller to fall back to its hardcoded default.
413
+ */
414
+ declare function resolveBasemap(config: AppConfig, variant: 'light' | 'dark', userId: string | undefined): BasemapConfig | undefined;
415
+ /**
416
+ * Merge an untrusted JSON value over a base config, field by field.
417
+ * Unknown fields are ignored. Malformed values fall back to the base.
418
+ * Never reads secrets.
419
+ */
420
+ declare function mergeAppConfig(base: AppConfig, override: unknown): AppConfig;
421
+
422
+ /**
423
+ * STAC (SpatioTemporal Asset Catalog) detection and parsing.
424
+ *
425
+ * Pure TypeScript helpers shared by ViewerRouter, StacMosaicViewer, and
426
+ * MultiCogViewer. No Svelte dependency, publishable via objex-utils.
427
+ */
428
+ /** STAC Link (shared by Catalog/Collection/Item). */
429
+ interface StacLink {
430
+ rel: string;
431
+ href: string;
432
+ type?: string;
433
+ title?: string;
434
+ }
435
+ /** STAC Item (GeoJSON Feature shape with stac_version). */
436
+ interface StacItem {
437
+ type: 'Feature';
438
+ stac_version: string;
439
+ id: string;
440
+ bbox?: [number, number, number, number];
441
+ geometry?: unknown;
442
+ properties?: Record<string, unknown>;
443
+ assets?: Record<string, StacAsset>;
444
+ collection?: string;
445
+ links?: StacLink[];
446
+ }
447
+ /** STAC FeatureCollection (collection of Items). Also used for STAC API responses. */
448
+ interface StacFeatureCollection {
449
+ type: 'FeatureCollection';
450
+ stac_version?: string;
451
+ features: StacItem[];
452
+ links?: StacLink[];
453
+ }
454
+ /** STAC Collection (a grouping of Items with its own metadata and links). */
455
+ interface StacCollection {
456
+ type: 'Collection';
457
+ stac_version: string;
458
+ id: string;
459
+ description?: string;
460
+ extent?: {
461
+ spatial?: {
462
+ bbox?: number[][];
463
+ };
464
+ temporal?: unknown;
465
+ };
466
+ links: StacLink[];
467
+ }
468
+ /** STAC Catalog (a directory-like grouping of Catalogs/Collections/Items via links). */
469
+ interface StacCatalog {
470
+ type: 'Catalog';
471
+ stac_version: string;
472
+ id: string;
473
+ description?: string;
474
+ links: StacLink[];
475
+ }
476
+ /** Single asset entry within a STAC item. */
477
+ interface StacAsset {
478
+ href: string;
479
+ type?: string;
480
+ title?: string;
481
+ roles?: string[];
482
+ /** Set when the asset carries `eo:bands`. */
483
+ 'eo:bands'?: {
484
+ name?: string;
485
+ common_name?: string;
486
+ }[];
487
+ }
488
+ /** Sentinel-2 band slot identifier, shared with utils/cog.ts composites. */
489
+ type BandSlot = 'red' | 'green' | 'blue' | 'nir' | 'swir1' | 'swir2' | 'rededge';
490
+ /** Parsed band map: slot name → HTTPS asset URL. */
491
+ type BandMap = Partial<Record<BandSlot, string>>;
492
+ /** Asset keys providers use for the single "display COG" asset, in priority order. */
493
+ declare const STAC_COG_ASSET_KEYS: readonly ["visual", "image", "data", "rendered_preview"];
494
+ /** Shape-check a parsed JSON object as a STAC Item. */
495
+ declare function isStacItem(json: unknown): json is StacItem;
496
+ /** Shape-check a parsed JSON object as a STAC FeatureCollection. */
497
+ declare function isStacFeatureCollection(json: unknown): json is StacFeatureCollection;
498
+ /** STAC Collection detection: `type === 'Collection'` + stac_version + links array. */
499
+ declare function isStacCollection(json: unknown): json is StacCollection;
500
+ /** STAC Catalog detection: `type === 'Catalog'` + stac_version + links array. */
501
+ declare function isStacCatalog(json: unknown): json is StacCatalog;
502
+ /** Routing verdict for any STAC-shaped JSON payload. */
503
+ type StacRoutableKind = {
504
+ kind: 'item';
505
+ item: StacItem;
506
+ } | {
507
+ kind: 'item-collection';
508
+ fc: StacFeatureCollection;
509
+ } | {
510
+ kind: 'collection';
511
+ payload: StacCollection;
512
+ } | {
513
+ kind: 'catalog';
514
+ payload: StacCatalog;
515
+ } | {
516
+ kind: 'none';
517
+ };
518
+ /** Classify an arbitrary parsed JSON into one of the STAC routing buckets. */
519
+ declare function classifyStac(json: unknown): StacRoutableKind;
520
+ /**
521
+ * Pick the COG-ish asset href from a STAC Item. Returns the href of the named
522
+ * asset when `preferred` is given and present, else scans STAC_COG_ASSET_KEYS,
523
+ * else falls back to any asset whose `type` contains "tiff". Returns null when
524
+ * nothing matches.
525
+ */
526
+ declare function pickCogAssetHref(item: StacItem, preferred?: string): string | null;
527
+ /** True when a single STAC Item exposes a COG-ish asset and a bbox. */
528
+ declare function detectMosaicCapable(item: StacItem): boolean;
529
+ /** True when a single STAC Item exposes ≥3 single-band raster COG assets,
530
+ * whether tagged with S2-style common names or not. The MultiCogViewer's
531
+ * band picker fills in the gap when presets don't auto-resolve. */
532
+ declare function detectMultiCogCapable(item: StacItem): boolean;
533
+ /** WGS84 bbox helper. Returns `null` if no bbox can be derived. */
534
+ declare function stacItemBbox(item: StacItem): [number, number, number, number] | null;
535
+ /** Normalized mosaic source entry consumed by MosaicLayer. */
536
+ interface MosaicSourceMeta {
537
+ id: string;
538
+ bbox: [number, number, number, number];
539
+ href: string;
540
+ }
541
+ /**
542
+ * Normalize a STAC Item or a plain `{id?, bbox, href}` record into a
543
+ * MosaicSourceMeta. Returns null when essentials (bbox / href) are missing.
544
+ */
545
+ declare function buildMosaicSourceMeta(input: StacItem | {
546
+ id?: string;
547
+ bbox: [number, number, number, number] | number[];
548
+ href: string;
549
+ }, assetKey?: string): MosaicSourceMeta | null;
550
+ /**
551
+ * Stable spatial cell key for an item, used to dedupe revisits when the
552
+ * caller wants only the freshest scene per footprint. STAC providers
553
+ * default to descending-datetime sort, so the first item per key is the
554
+ * newest. Prefers STAC `grid:code`, then Sentinel-2 MGRS triplet, then
555
+ * `s2:mgrs_tile`, then a rounded bbox so non-S2 providers still dedupe.
556
+ */
557
+ declare function spatialCellKey(item: StacItem, bbox: [number, number, number, number]): string;
558
+ /**
559
+ * Map Sentinel-2 STAC item assets to a BandMap. Recognizes:
560
+ * - `eo:bands[0].common_name` (preferred, stable across providers)
561
+ * - asset key heuristics for Microsoft PC / Element 84 / AWS S2 L2A buckets
562
+ * Returns an empty map when no bands are identifiable so callers can fall
563
+ * back to a different viewer.
564
+ */
565
+ declare function extractSentinelBandAssets(item: StacItem): BandMap;
566
+ /** True when the band map has enough channels for a True Color composite. */
567
+ declare function hasRgbBands(map: BandMap): boolean;
568
+ /**
569
+ * Generic raster band asset description, vendor-neutral. Carries the bits a
570
+ * MultiCOG band picker needs to populate dropdowns and resolve presets.
571
+ *
572
+ * `key` is the STAC asset key as it appears in `item.assets` (`red`, `B04`,
573
+ * `image`, `analytic`, ...). It is the natural identifier and lines up 1:1
574
+ * with `MultiCOGLayer.sources`'s record key.
575
+ */
576
+ interface RasterBandAsset {
577
+ key: string;
578
+ href: string;
579
+ commonName?: string;
580
+ bandCount?: number;
581
+ roles?: string[];
582
+ mediaType?: string;
583
+ title?: string;
584
+ }
585
+ /**
586
+ * Enumerate every asset on a STAC Item that looks like a single-band raster
587
+ * COG suitable for compositing in MultiCOGLayer. Used by the band picker UI.
588
+ *
589
+ * Inclusion: `media_type` matches `image/tiff*` (drops JP2 mirrors, PNGs,
590
+ * thumbnails) AND it is not a thumbnail / overview / metadata role. Pre-baked
591
+ * multi-band visuals (`raster:bands.length > 1` or `eo:bands.length > 1`) are
592
+ * dropped because compositing needs single-band sources, not a 3-band visual
593
+ * COG; that asset belongs in `CogViewer`.
594
+ *
595
+ * `bandCount` is reported as `1` when neither tag is present and the asset
596
+ * media type is COG-like, so vendor catalogs that omit `eo:bands` are still
597
+ * pickable. Callers wanting to be strict can filter on `bandCount === 1`.
598
+ */
599
+ declare function extractRasterBandAssets(item: StacItem): RasterBandAsset[];
600
+ /**
601
+ * Resolve a semantic band slot to an asset key on this Item.
602
+ *
603
+ * Priority:
604
+ * 1. First asset whose `eo:bands[0].common_name` (lowercased) equals the slot.
605
+ * 2. First asset whose key appears in `BAND_KEY_FALLBACKS[slot]` (vendor key
606
+ * conventions: `B04`, `red-jp2`, etc.).
607
+ *
608
+ * Returns the asset key (NOT the href) so callers can plumb it into
609
+ * `composite: { r, g, b }` and look it up in `extractRasterBandAssets()`.
610
+ */
611
+ declare function resolveBandSlotAssetKey(assets: RasterBandAsset[], slot: BandSlot): string | undefined;
612
+ /**
613
+ * Resolve a preset's R/G/B BandSlot triple into asset keys for this Item.
614
+ * Returns null when any of the three required slots cannot be resolved, so
615
+ * the caller can disable the preset rather than half-apply it.
616
+ */
617
+ declare function resolvePresetComposite(assets: RasterBandAsset[], composite: {
618
+ r: BandSlot;
619
+ g: BandSlot;
620
+ b: BandSlot;
621
+ }): {
622
+ r: string;
623
+ g: string;
624
+ b: string;
625
+ } | null;
626
+ /** True when ≥3 single-band raster COG assets exist, so manual RGB pick is viable. */
627
+ declare function hasCompositableBands(assets: RasterBandAsset[]): boolean;
628
+ /**
629
+ * Same shape as `extractRasterBandAssets`, but does NOT drop multi-band assets.
630
+ * Used by the mosaic asset picker, where a 3-band pre-baked `visual` TCI is a
631
+ * legitimate choice alongside the per-band single-band COGs.
632
+ *
633
+ * `bandCount` is reported when known (`raster:bands.length` or `eo:bands.length`),
634
+ * else left undefined so the consuming UI can fall back to probing the COG.
635
+ */
636
+ declare function extractMosaicAssets(item: StacItem): RasterBandAsset[];
637
+
638
+ /**
639
+ * Generic per-channel asset descriptor for the unified RGB picker.
640
+ *
641
+ * Pure TypeScript. No Svelte dependency. Publishable via objex-utils.
642
+ *
643
+ * `CogAsset` is the canonical shape every viewer (CogViewer, MultiCogViewer,
644
+ * StacMosaicViewer) hands to the shared ChannelPicker UI. Each entry records
645
+ * the STAC asset key (`red`, `B04`, `image`, `visual`, ... or `self` when the
646
+ * viewer is a single bare COG file with no STAC context), the href, the band
647
+ * count (from `raster:bands.length` when STAC populates it, lazily probed from
648
+ * the COG header otherwise), and the optional `eo:bands` common name.
649
+ */
650
+
651
+ interface CogAsset {
652
+ /** STAC asset key, or `self` for a single bare COG without STAC context. */
653
+ key: string;
654
+ /** Absolute or relative href as it appears in the STAC item / URL. */
655
+ href: string;
656
+ /** Number of bands in the asset. 1 by default until probed. */
657
+ bandCount: number;
658
+ /** True when bandCount came from STAC metadata or a probe; false → trust default. */
659
+ bandCountKnown: boolean;
660
+ /** `raster:bands[0].data_type` if known. */
661
+ dtype?: string;
662
+ /** `eo:bands[].common_name` lowercased, aligned to bandIndex order. */
663
+ eoCommon: string[];
664
+ /** STAC asset roles (`data`, `visual`, `reflectance`, ...). */
665
+ roles: string[];
666
+ /** Optional human title. */
667
+ title?: string;
668
+ /** Asset media_type as advertised by STAC. */
669
+ mediaType?: string;
670
+ }
671
+ /** Per-channel pixel coordinate inside a STAC item. */
672
+ interface ChannelRef {
673
+ assetKey: string;
674
+ bandIndex: number;
675
+ }
676
+ /** RGB(A) composite expressed as `(asset, bandIndex)` per channel. */
677
+ interface ChannelComposite {
678
+ r: ChannelRef;
679
+ g: ChannelRef;
680
+ b: ChannelRef;
681
+ a?: ChannelRef;
682
+ }
683
+ /**
684
+ * Enumerate every TIFF/COG asset on a STAC Item, keeping multi-band assets
685
+ * (NAIP `image`, S2 `visual` TCI) alongside single-band per-band assets.
686
+ *
687
+ * Reads band metadata from (in priority order):
688
+ * 1. `asset.bands` (STAC 1.1 unified bands array)
689
+ * 2. `asset['raster:bands']` (STAC 1.0 raster extension)
690
+ * 3. `asset['eo:bands']` (STAC 1.0 eo extension)
691
+ * 4. `item.properties.bands` (STAC 1.1 item-level bands, applies to all
692
+ * assets that do not override) — covers catalogs like the Hamilton
693
+ * NAIP-style 3-inch where each item has 4 bands but the single `data`
694
+ * asset carries no band metadata of its own.
695
+ *
696
+ * `bandCount` is set when any of the above provides one; otherwise defaults
697
+ * to 1 with `bandCountKnown: false` so callers can lazily probe on first pick.
698
+ */
699
+ declare function extractCogAssets(item: StacItem): CogAsset[];
700
+ /**
701
+ * For `CogViewer` (single bare COG file, no STAC context). Returns one synthetic
702
+ * asset with key `self` so the same ChannelPicker UI works without special-casing.
703
+ * `bandCount` defaults to 1, set to the probed `geotiff.count` once known.
704
+ */
705
+ declare function syntheticSelfAsset(href: string, probedBandCount?: number): CogAsset;
706
+ /**
707
+ * Pick the most natural and most performant default composite for an item.
708
+ *
709
+ * Priority (first match wins):
710
+ * 1. A 3-band uint8 pre-baked visual asset (`visual` / `image` / `tci` etc):
711
+ * all three channels bind to it, bands 0/1/2. Single COGLayer path,
712
+ * one decoder, fastest.
713
+ * 2. Common-name red/green/blue resolvable across separate single-band assets.
714
+ * MultiCOGLayer path.
715
+ * 3. Fallback: first three raster assets, band 0 each.
716
+ *
717
+ * Returns null when no raster assets exist.
718
+ */
719
+ declare function pickNaturalColorComposite(assets: CogAsset[]): {
720
+ composite: ChannelComposite;
721
+ source: 'visual-asset' | 'rgb-bands' | 'fallback';
722
+ } | null;
723
+ /** True when all three RGB channels target the same asset key. */
724
+ declare function isSingleAssetComposite(c: ChannelComposite): boolean;
725
+ /** True when all three channels are at band index 0 (the MultiCOGLayer-compatible case). */
726
+ declare function allChannelsBand0(c: ChannelComposite): boolean;
727
+
728
+ /**
729
+ * Preset definitions, URL round-trip, and preset application for the unified
730
+ * RGB picker. Pure TypeScript, publishable via objex-utils.
731
+ *
732
+ * Presets describe a SEMANTIC band slot triple (`red`/`green`/`blue` for
733
+ * Natural Color, `nir`/`red`/`green` for False-Color IR, ...). Resolving a
734
+ * preset against a specific item walks `BAND_KEY_FALLBACKS` in `utils/stac.ts`
735
+ * to map slots to actual asset keys on that item. NDVI and other single-band
736
+ * derived presets are intentionally NOT in this list for this slice.
737
+ */
738
+
739
+ interface PresetDef {
740
+ id: string;
741
+ labelKey: string;
742
+ slots: {
743
+ r: BandSlot;
744
+ g: BandSlot;
745
+ b: BandSlot;
746
+ };
747
+ }
748
+ declare const PRESETS: PresetDef[];
749
+ /** Subset of PRESETS whose slot triple resolves on this item. */
750
+ declare function availablePresets(assets: CogAsset[]): PresetDef[];
751
+ /** Resolve a preset to a ChannelComposite for this item. Returns null when not applicable. */
752
+ declare function applyPreset(assets: CogAsset[], preset: PresetDef): ChannelComposite | null;
753
+ /** True when the preset's resolved composite still matches the user's current picks. */
754
+ declare function presetMatchesComposite(preset: PresetDef, c: ChannelComposite, assets: CogAsset[]): boolean;
755
+ /**
756
+ * Decode a `URLSearchParams` chunk into a ChannelComposite.
757
+ *
758
+ * Format: `r=<asset>&g=<asset>&b=<asset>&band_r=<n>&band_g=<n>&band_b=<n>` plus
759
+ * optional `a=<asset>&band_a=<n>`. `band_*` defaults to 0 when absent so
760
+ * legacy MultiCog URLs (`?r=red&g=green&b=blue&preset=true-color`) keep
761
+ * round-tripping. Returns null when any required asset key is missing from
762
+ * the current item's asset list.
763
+ */
764
+ declare function compositeFromUrl(params: URLSearchParams, assets: CogAsset[]): ChannelComposite | null;
765
+ /** Encode a composite + active preset id into URLSearchParams for the hash. */
766
+ declare function compositeToUrl(c: ChannelComposite, presetId: string | null): URLSearchParams;
767
+
768
+ /**
769
+ * Copy text to clipboard and run a feedback callback for COPY_FEEDBACK_MS.
770
+ * Silently catches clipboard errors (e.g. insecure context).
771
+ *
772
+ * @returns true if copy succeeded, false otherwise.
773
+ */
774
+ declare function copyToClipboard(text: string, onFeedback?: (copied: boolean) => void): Promise<boolean>;
775
+ /**
776
+ * Wire click-to-copy on all elements matching `selector` inside `root`.
777
+ * Each element must have `data-code` (URI-encoded) with the text to copy.
778
+ * Adds/removes a `copied` CSS class for visual feedback.
779
+ */
780
+ declare function wireCodeCopyButtons(root: Element, selector: string): void;
781
+
347
782
  /**
348
783
  * Cloud storage protocol URL utilities — pure TS, no Svelte dependency.
349
784
  *
@@ -404,9 +839,69 @@ declare function typeColor(category: TypeCategory): string;
404
839
  declare function typeBadgeClass(category: TypeCategory): string;
405
840
  declare function typeLabel(category: TypeCategory): string;
406
841
 
842
+ /**
843
+ * Canonical connection identity.
844
+ *
845
+ * Single source of truth for deciding when two connection configs point at
846
+ * the same bucket. Used by the connections store to deduplicate auto-detect,
847
+ * manual add, and edit flows, so one physical bucket never ends up with
848
+ * multiple competing local records.
849
+ *
850
+ * Identity rules, per provider:
851
+ *
852
+ * azure → (provider, endpoint, bucket) endpoint carries the account
853
+ * gcs → (provider, bucket) GCS bucket names are global
854
+ * s3 → (provider, bucket, region) AWS native: same bucket name
855
+ * can exist in exactly one region,
856
+ * but the region is load-bearing
857
+ * for signing, so a paste with a
858
+ * different region is a distinct
859
+ * connection until the user merges
860
+ * other → (provider, endpoint, bucket) r2, b2, minio, wasabi, storj,
861
+ * digitalocean, contabo, hetzner,
862
+ * linode, ovhcloud, custom
863
+ *
864
+ * Endpoint normalization is aggressive: scheme + host + non-default port +
865
+ * pathname, with trailing slashes and default ports stripped, host lowercased.
866
+ * That collapses the common trip hazards — http vs https, :443 vs empty,
867
+ * trailing slash drift, mixed case host.
868
+ */
869
+
870
+ interface ConnectionIdentityInput {
871
+ provider: string;
872
+ endpoint: string;
873
+ bucket: string;
874
+ region: string;
875
+ }
876
+ /**
877
+ * Normalize an endpoint URL to a canonical form suitable for equality checks.
878
+ * Empty / whitespace-only input returns `''` (the "no endpoint" sentinel).
879
+ * Non-URL strings are lowercased and stripped of trailing slashes as a best
880
+ * effort so the comparison is still deterministic.
881
+ */
882
+ declare function normalizeEndpoint(raw: string | undefined | null): string;
883
+ /** Collapse unknown / empty providers to `'s3'`; otherwise lowercase. */
884
+ declare function normalizeProvider(provider: string | undefined | null): ProviderId;
885
+ /**
886
+ * Produce a canonical key for a connection's identity. Two connection
887
+ * configs with the same identity key point at the same physical bucket.
888
+ * Returns `''` when the config is too incomplete to identify a bucket.
889
+ */
890
+ declare function connectionIdentityKey(input: ConnectionIdentityInput): string;
891
+ /** Convenience: true when both inputs share the same non-empty identity. */
892
+ declare function isSameConnectionIdentity(a: ConnectionIdentityInput, b: ConnectionIdentityInput): boolean;
893
+
407
894
  /**
408
895
  * Shared error handling for async viewer load operations.
409
896
  */
897
+ /**
898
+ * True for any abort cascade. Recognizes raw `DOMException(AbortError)`,
899
+ * objects whose `.name` is `AbortError`, deck.gl's `_SourceError("Failed
900
+ * to fetch")` wrapper whose `cause` is an AbortError, and free-text errors
901
+ * from `@developmentseed/geotiff` that mention "aborted". Used to silence
902
+ * cancellation noise without swallowing real failures.
903
+ */
904
+ declare function isAbortError(err: unknown): boolean;
410
905
  /**
411
906
  * Extract an error message from an unknown caught value.
412
907
  * Returns null for AbortError (caller should silently return).
@@ -515,6 +1010,59 @@ declare function buildGeoArrowTables(wkbArrays: Uint8Array[], attributes: Map<st
515
1010
  type: string;
516
1011
  }>, knownGeomType?: GeoArrowGeomType): GeoArrowResult[];
517
1012
 
1013
+ /**
1014
+ * Helpers for parsing DuckDB v1.5 parameterized GEOMETRY type strings.
1015
+ *
1016
+ * DuckDB v1.5 made GEOMETRY a core type with an optional CRS parameter:
1017
+ * GEOMETRY — no CRS attached
1018
+ * GEOMETRY('EPSG:4326') — EPSG form
1019
+ * GEOMETRY('OGC:CRS84') — OGC form (canonical for GeoParquet 1.1+)
1020
+ * GEOMETRY('EPSG:27700') — projected CRS
1021
+ *
1022
+ * Type strings may come from `DESCRIBE`, from the Arrow schema, or from a
1023
+ * legacy code path that still reports `BLOB`. Use these helpers everywhere
1024
+ * instead of ad-hoc regex so behaviour stays consistent.
1025
+ */
1026
+ interface GeometryTypeInfo {
1027
+ /** True if the type is some form of GEOMETRY (with or without CRS). */
1028
+ isGeometry: boolean;
1029
+ /** True if the type carries a CRS parameter, e.g. GEOMETRY('EPSG:4326'). */
1030
+ hasCrs: boolean;
1031
+ /** The CRS string if present, otherwise null. Raw value, including WGS84. */
1032
+ rawCrs: string | null;
1033
+ /**
1034
+ * The CRS string if present and NOT a WGS84 variant (EPSG:4326, EPSG:4979,
1035
+ * OGC:CRS84). Returns null for WGS84 so callers can skip ST_Transform.
1036
+ */
1037
+ nonWgs84Crs: string | null;
1038
+ }
1039
+ /**
1040
+ * Parse a DuckDB type string and report whether it is a GEOMETRY type, and
1041
+ * whether a CRS parameter is attached.
1042
+ */
1043
+ declare function parseGeometryTypeCrs(typeStr: string | null | undefined): GeometryTypeInfo;
1044
+ /** True for EPSG:4326, EPSG:4979, OGC:CRS84 and equivalent strings. */
1045
+ declare function isWgs84Crs(crs: string | null | undefined): boolean;
1046
+ /**
1047
+ * Build a `ST_Transform(...)` SQL expression choosing the 2-arg form when the
1048
+ * input already carries its CRS in the GEOMETRY type (DuckDB v1.5), and the
1049
+ * 3-arg form otherwise.
1050
+ *
1051
+ * `geometry_always_xy` is set globally at DB init, so no per-call `always_xy`
1052
+ * argument is needed.
1053
+ */
1054
+ declare function buildTransformExpr(innerExpr: string, sourceType: string, sourceCrs: string, targetCrs: string): string;
1055
+ /**
1056
+ * Wrap a bare WKB expression with `ST_SetCRS(ST_GeomFromWKB(...))` so that the
1057
+ * resulting GEOMETRY value carries a CRS through the rest of the pipeline.
1058
+ * Used in the legacy GeoParquet fallback where we read the geometry column as
1059
+ * BLOB but still know the source CRS from hyparquet metadata or the GeoParquet
1060
+ * footer.
1061
+ *
1062
+ * If `sourceCrs` is null/empty, returns a plain `ST_GeomFromWKB(...)`.
1063
+ */
1064
+ declare function wrapWkbWithCrs(wkbExpr: string, sourceCrs: string | null | undefined): string;
1065
+
518
1066
  interface HexRow {
519
1067
  offset: string;
520
1068
  hex: string[];
@@ -526,6 +1074,142 @@ interface HexRow {
526
1074
  */
527
1075
  declare function generateHexDump(data: Uint8Array, bytesPerRow?: number): HexRow[];
528
1076
 
1077
+ /**
1078
+ * Universal cloud storage URL / bucket parser.
1079
+ *
1080
+ * Accepts the many URI/URL formats that users commonly paste and extracts
1081
+ * the correct bucket, region, endpoint, and provider.
1082
+ *
1083
+ * Supported URI schemes:
1084
+ * s3:// s3a:// s3n:// aws:// — Amazon S3 / S3-compatible
1085
+ * r2:// — Cloudflare R2
1086
+ * gs:// gcs:// — Google Cloud Storage
1087
+ * azure:// az:// — Azure Blob Storage
1088
+ * abfs:// abfss:// — Azure Data Lake (ADLS Gen2)
1089
+ * wasbs:// — Azure Blob (Hadoop WASB driver)
1090
+ * swift:// — OpenStack Swift
1091
+ * file:// filesystem:// — Local filesystem
1092
+ *
1093
+ * Supported HTTPS URL patterns:
1094
+ * https://<bucket>.s3.<region>.amazonaws.com[/prefix] — AWS virtual-hosted
1095
+ * https://s3.<region>.amazonaws.com/<bucket>[/prefix] — AWS path-style
1096
+ * https://s3.amazonaws.com/<bucket> — AWS global
1097
+ * https://<account>.r2.cloudflarestorage.com/<bucket> — Cloudflare R2
1098
+ * https://storage.googleapis.com/<bucket> — Google Cloud Storage
1099
+ * https://<bucket>.storage.googleapis.com[/prefix] — GCS virtual-hosted
1100
+ * https://<bucket>.<region>.digitaloceanspaces.com — DigitalOcean Spaces
1101
+ * https://<region>.digitaloceanspaces.com/<bucket> — DO Spaces path-style
1102
+ * https://s3.<region>.wasabisys.com/<bucket> — Wasabi
1103
+ * https://f<id>.backblazeb2.com/file/<bucket> — Backblaze B2
1104
+ * https://<bucket>.s3.<region>.backblazeb2.com — B2 S3-compatible
1105
+ * https://<bucket>.oss-<region>.aliyuncs.com — Alibaba Cloud OSS
1106
+ * https://<bucket>.cos.<region>.myqcloud.com — Tencent COS
1107
+ * https://storage.yandexcloud.net/<bucket> — Yandex Cloud
1108
+ * https://gateway.storjshare.io/<bucket> — Storj S3 gateway
1109
+ * https://link.storjshare.io/raw/<access>/<bucket> — Storj linksharing
1110
+ * https://<custom-endpoint>/<bucket> — Generic S3-compatible
1111
+ *
1112
+ * Also handles plain bucket names (no protocol).
1113
+ */
1114
+ type StorageProvider = string;
1115
+ interface ParsedStorageUrl {
1116
+ bucket: string;
1117
+ region: string;
1118
+ endpoint: string;
1119
+ provider: StorageProvider;
1120
+ /** Original prefix/path after bucket, if any */
1121
+ prefix: string;
1122
+ }
1123
+ /** STAC API path test, one source of truth. Tests pathname only. */
1124
+ declare const STAC_API_PATH_RE: RegExp;
1125
+ /**
1126
+ * Returns true when the host matches any of the provider host patterns
1127
+ * that `parseStorageUrl` recognizes on the HTTPS branch.
1128
+ */
1129
+ declare function isKnownBucketHost(host: string): boolean;
1130
+ interface Defaults {
1131
+ region?: string;
1132
+ endpoint?: string;
1133
+ provider?: StorageProvider;
1134
+ }
1135
+ /**
1136
+ * Parse a user-provided bucket/URL string into structured storage connection parts.
1137
+ */
1138
+ declare function parseStorageUrl(input: string, defaults?: Defaults): ParsedStorageUrl;
1139
+ /**
1140
+ * Returns true if the input looks like a URL/URI rather than a plain bucket name.
1141
+ * Covers all recognized cloud storage URI schemes.
1142
+ */
1143
+ declare function looksLikeUrl(input: string): boolean;
1144
+ /**
1145
+ * Given a parsed URL result, build a human-readable summary of what was detected.
1146
+ */
1147
+ declare function describeParseResult(parsed: ParsedStorageUrl): string;
1148
+ type UrlClassification = {
1149
+ kind: 'scheme';
1150
+ parsed: ParsedStorageUrl;
1151
+ } | {
1152
+ kind: 'object-storage';
1153
+ parsed: ParsedStorageUrl;
1154
+ } | {
1155
+ kind: 'stac-api';
1156
+ url: URL;
1157
+ } | {
1158
+ kind: 'remote-file';
1159
+ url: URL;
1160
+ };
1161
+ /**
1162
+ * Classify a user-supplied URL/URI into one of four buckets. Unparseable or
1163
+ * plain inputs fall through to `remote-file` with a best-effort URL parse,
1164
+ * returning a synthetic `https://` URL when `new URL()` would throw.
1165
+ */
1166
+ declare function classifyUrl(input: string): UrlClassification;
1167
+
1168
+ /**
1169
+ * Auto-detect hosting bucket from URL search params and window.location.
1170
+ *
1171
+ * Detection priority:
1172
+ * 1. `?url=<storage-url>` query parameter (highest priority)
1173
+ * 2. `window.location.hostname` pattern matching (fallback)
1174
+ *
1175
+ * Also extracts `rootPrefix` when the app is hosted inside a subfolder.
1176
+ */
1177
+
1178
+ interface DetectedHost {
1179
+ provider: StorageProvider;
1180
+ bucket: string;
1181
+ region: string;
1182
+ endpoint: string;
1183
+ rootPrefix: string;
1184
+ bucketUrl: string;
1185
+ }
1186
+ /**
1187
+ * Detect hosting bucket from current URL.
1188
+ * Returns null when no hosting bucket can be determined.
1189
+ */
1190
+ declare function detectHostBucket(): DetectedHost | null;
1191
+ /**
1192
+ * Enrich an in-progress connection-config draft with hints from a STAC Item
1193
+ * that declares the Storage Extension (`storage:region`,
1194
+ * `storage:requester_pays`, `storage:platform`, v2 `storage:schemes`).
1195
+ *
1196
+ * Modular by design, callers opt in. The existing `detectHostBucket` flow
1197
+ * does NOT know about STAC, so call sites that already hold a representative
1198
+ * `StacItem` (e.g. classification just after fetching a Catalog / Collection /
1199
+ * ItemCollection) should funnel through this helper before handing the draft
1200
+ * to `connectionStore.saveHostConnection` / `connectionStore.save`. Existing
1201
+ * non-empty fields on the draft are preserved (this never clobbers a
1202
+ * user-set value).
1203
+ *
1204
+ * Returns a shallow copy of `input` with hint fields filled in. Safe to call
1205
+ * with an unrelated item, returns `input` untouched when the extension is
1206
+ * absent or unparseable.
1207
+ */
1208
+ declare function applyStacItemStorageHints<T extends {
1209
+ region?: string;
1210
+ endpoint?: string;
1211
+ }>(input: T, item: StacItem): T;
1212
+
529
1213
  /**
530
1214
  * Generic localStorage helpers with SSR safety.
531
1215
  *
@@ -543,6 +1227,93 @@ declare function loadFromStorage<T>(key: string, defaultValue: T): T;
543
1227
  */
544
1228
  declare function persistToStorage(key: string, value: unknown): void;
545
1229
 
1230
+ /**
1231
+ * Tiny insertion-order LRU built on top of `Map`. `get` / `has` move the entry
1232
+ * to the most-recent slot, `set` evicts the oldest (and runs `onEvict`) once
1233
+ * the cap is exceeded.
1234
+ *
1235
+ * Keeps the implementation deliberately small. Used by viewer modules that
1236
+ * cache per-source resources (GeoTIFF headers, presigned URLs) which would
1237
+ * otherwise leak across long pan / viewport-reload sessions.
1238
+ */
1239
+ interface LruCacheOptions<K, V> {
1240
+ /** Maximum number of entries. Must be > 0. */
1241
+ max: number;
1242
+ /** Called when an entry is evicted (LRU overflow or `delete()`). */
1243
+ onEvict?: (key: K, value: V) => void;
1244
+ }
1245
+ declare class LruCache<K, V> {
1246
+ private map;
1247
+ readonly max: number;
1248
+ private onEvict?;
1249
+ constructor(opts: LruCacheOptions<K, V>);
1250
+ get size(): number;
1251
+ has(key: K): boolean;
1252
+ get(key: K): V | undefined;
1253
+ set(key: K, value: V): void;
1254
+ delete(key: K): boolean;
1255
+ clear(): void;
1256
+ }
1257
+
1258
+ /**
1259
+ * Framework-agnostic helper that wires a "click on map, run a probe, surface the
1260
+ * result" flow used by every COG-style viewer (`CogViewer`, `StacMosaicViewer`,
1261
+ * `MultiCogViewer`).
1262
+ *
1263
+ * The viewers differ in what the probe returns (a single pixel sample, a per-
1264
+ * channel fan-out, or a topmost-source bbox hit), but they share the same
1265
+ * boilerplate: subscribe to `click`, mark inspecting, await the probe, surface
1266
+ * the payload, abort the previous probe if a new click arrives mid-flight, and
1267
+ * tear the listener down on cleanup.
1268
+ *
1269
+ * No dependency on Svelte, MapLibre, or deck.gl. The `MapLike` shape captures
1270
+ * only what the helper needs from the underlying map so this can be unit-tested
1271
+ * with a tiny stub if needed.
1272
+ */
1273
+ interface PixelInspectClickEvent {
1274
+ lngLat: {
1275
+ lng: number;
1276
+ lat: number;
1277
+ };
1278
+ }
1279
+ type PixelInspectClickHandler = (event: PixelInspectClickEvent) => void;
1280
+ /**
1281
+ * Minimal subset of MapLibre's map API used by the inspector. Anything that
1282
+ * dispatches a `click` event with `{lngLat}` and supports symmetric on/off
1283
+ * registration plugs in.
1284
+ */
1285
+ interface MapLike {
1286
+ on(type: 'click', handler: PixelInspectClickHandler): unknown;
1287
+ off(type: 'click', handler: PixelInspectClickHandler): unknown;
1288
+ }
1289
+ interface PixelInspectProbeRequest {
1290
+ lng: number;
1291
+ lat: number;
1292
+ signal: AbortSignal;
1293
+ }
1294
+ type PixelInspectProbe<T> = (req: PixelInspectProbeRequest) => Promise<T | null>;
1295
+ interface PixelInspectCallbacks<T> {
1296
+ /** Called synchronously when a click is accepted, before the probe is awaited. */
1297
+ onStart(): void;
1298
+ /**
1299
+ * Called once per click after the probe settles. Receives `null` when the
1300
+ * probe returned `null` or threw a non-helper-driven error (including an
1301
+ * `AbortError` that did not originate from this helper's own controller).
1302
+ */
1303
+ onResult(result: T | null): void;
1304
+ }
1305
+ interface AttachPixelInspectorOptions<T> {
1306
+ probe: PixelInspectProbe<T>;
1307
+ onStart: PixelInspectCallbacks<T>['onStart'];
1308
+ onResult: PixelInspectCallbacks<T>['onResult'];
1309
+ }
1310
+ /**
1311
+ * Wire a click-to-inspect probe onto `map`. Returns a `detach()` function that
1312
+ * removes the listener AND aborts any in-flight probe. Subsequent clicks abort
1313
+ * the previous probe so a fast double-click never leaves a stale result behind.
1314
+ */
1315
+ declare function attachPixelInspector<T>(map: MapLike, { probe, onStart, onResult }: AttachPixelInspectorOptions<T>): () => void;
1316
+
546
1317
  interface SqlBlock {
547
1318
  name: string;
548
1319
  sql: string;
@@ -574,6 +1345,92 @@ declare function interpolateTemplates(text: string, queryResults: Map<string, Re
574
1345
  */
575
1346
  declare function markSqlBlocks(content: string): string;
576
1347
 
1348
+ /**
1349
+ * Executes the SQL blocks parsed out of a markdown document (Evidence.dev style)
1350
+ * against an injected query engine, and caches the results by block name. Pairs
1351
+ * with `markdown-sql.ts` (the parser). Pure TypeScript, the engine is supplied by
1352
+ * the host so this module never imports DuckDB or any other heavy dependency.
1353
+ */
1354
+ declare class MarkdownSqlContext {
1355
+ private engine;
1356
+ private connId;
1357
+ private prefix;
1358
+ private results;
1359
+ constructor(engine: QueryEngine, connId: string, prefix?: string);
1360
+ /** Execute a SQL query and store the result under the given name. */
1361
+ executeSql(sql: string, queryName: string): Promise<Record<string, any>[]>;
1362
+ /**
1363
+ * Transform relative file paths in SQL to full S3 URLs.
1364
+ * e.g. read_parquet('data.parquet') becomes read_parquet('s3://bucket/prefix/data.parquet').
1365
+ */
1366
+ private transformPaths;
1367
+ getResult(queryName: string): {
1368
+ result: QueryResult;
1369
+ rows: Record<string, any>[];
1370
+ } | undefined;
1371
+ getAllResults(): Map<string, Record<string, any>[]>;
1372
+ getColumns(queryName: string): string[];
1373
+ }
1374
+
1375
+ /**
1376
+ * Lightweight browser-native Jupyter notebook renderer.
1377
+ * Replaces `notebookjs` which depends on jsdom/Buffer (Node.js only).
1378
+ * Handles nbformat 2, 3, 4, and 5.
1379
+ */
1380
+ interface NotebookConfig {
1381
+ markdown: (md: string) => string;
1382
+ ansi: (text: string) => string;
1383
+ highlighter: (code: string, lang: string) => string;
1384
+ }
1385
+ interface RawNotebook {
1386
+ nbformat: number;
1387
+ nbformat_minor?: number;
1388
+ metadata?: Record<string, any>;
1389
+ cells?: RawCell[];
1390
+ worksheets?: {
1391
+ cells: RawCell[];
1392
+ }[];
1393
+ }
1394
+ interface RawCell {
1395
+ cell_type: string;
1396
+ source?: string | string[];
1397
+ input?: string | string[];
1398
+ outputs?: RawOutput[];
1399
+ prompt_number?: number;
1400
+ execution_count?: number | null;
1401
+ level?: number;
1402
+ language?: string;
1403
+ }
1404
+ interface RawOutput {
1405
+ output_type: string;
1406
+ data?: Record<string, string | string[]>;
1407
+ text?: string | string[];
1408
+ stream?: string;
1409
+ name?: string;
1410
+ png?: string;
1411
+ jpeg?: string;
1412
+ svg?: string;
1413
+ html?: string;
1414
+ latex?: string;
1415
+ traceback?: string[];
1416
+ ename?: string;
1417
+ evalue?: string;
1418
+ [key: string]: any;
1419
+ }
1420
+ interface NotebookMeta {
1421
+ kernelName: string;
1422
+ language: string;
1423
+ cellCount: number;
1424
+ }
1425
+ /**
1426
+ * Parse and render a Jupyter notebook JSON to a DOM element.
1427
+ * Returns the rendered element and metadata.
1428
+ */
1429
+ declare function renderNotebook(raw: RawNotebook, config: NotebookConfig): {
1430
+ element: HTMLElement;
1431
+ meta: NotebookMeta;
1432
+ };
1433
+
577
1434
  /**
578
1435
  * Lightweight Parquet metadata reader using hyparquet.
579
1436
  *
@@ -640,42 +1497,185 @@ declare function extractGeometryTypes(geo: GeoParquetMeta): GeoArrowGeomType[];
640
1497
  declare function extractBounds(geo: GeoParquetMeta): [number, number, number, number] | null;
641
1498
 
642
1499
  /**
643
- * STAC (SpatioTemporal Asset Catalog) detection and parsing.
1500
+ * STAC item facets, filtering, and sorting. Pure TS, framework-agnostic.
644
1501
  *
645
- * Pure TypeScript helpers shared by ViewerRouter, StacMosaicViewer, and
646
- * MultiCogViewer. No Svelte dependency, publishable via objex-utils.
1502
+ * Inputs are STAC Items (or any subset compatible with `StacItem`). Outputs
1503
+ * are slim views, auto-detected facet descriptors, and filtered/sorted view
1504
+ * arrays. No DOM, no Svelte, no fetch, no maplibre, safe to publish via
1505
+ * `@walkthru-earth/objex-utils`.
1506
+ *
1507
+ * The flow is intentionally one-directional:
1508
+ * StacItem[] --extractItemView--> StacItemView[]
1509
+ * StacItemView[] --buildFacets--> FacetSet
1510
+ * StacItemView[] + FacetState --applyFacets--> StacItemView[]
1511
+ * StacItemView[] + FacetSort --sortViews--> StacItemView[]
1512
+ *
1513
+ * Callers (any framework) can hold the views array, derive a FacetSet on
1514
+ * change, and reactively filter / sort it without re-touching the original
1515
+ * StacItems.
647
1516
  */
648
- /** STAC Link (shared by Catalog/Collection/Item). */
649
- interface StacLink {
650
- rel: string;
651
- href: string;
652
- type?: string;
653
- title?: string;
654
- }
655
- /** STAC Item (GeoJSON Feature shape with stac_version). */
656
- interface StacItem {
657
- type: 'Feature';
658
- stac_version: string;
1517
+
1518
+ /**
1519
+ * Compact, render-ready projection of a STAC Item. Keeps only the fields
1520
+ * needed for facet UI, sorting, footprint rendering, and the inspector
1521
+ * panel. The full original item is retained on `raw` for callers that need
1522
+ * to inspect arbitrary properties without a re-extract pass.
1523
+ */
1524
+ interface StacItemView {
659
1525
  id: string;
660
- bbox?: [number, number, number, number];
661
- geometry?: unknown;
662
- properties?: Record<string, unknown>;
663
- assets?: Record<string, StacAsset>;
664
- collection?: string;
665
- links?: StacLink[];
1526
+ collection: string | null;
1527
+ bbox: [number, number, number, number] | null;
1528
+ /** ISO 8601 datetime, or `start_datetime` when only an interval is given. */
1529
+ datetime: string | null;
1530
+ /** End of `start_datetime` / `end_datetime` interval, when present. */
1531
+ endDatetime: string | null;
1532
+ /** `eo:cloud_cover` percent (0-100), null when absent. */
1533
+ cloudCover: number | null;
1534
+ /** Ground sample distance in meters, null when absent. */
1535
+ gsd: number | null;
1536
+ platform: string | null;
1537
+ constellation: string | null;
1538
+ instruments: string[];
1539
+ /** EPSG code from `proj:epsg`, null when absent or non-numeric. */
1540
+ epsg: number | null;
1541
+ /** Best-effort thumbnail / overview href, null when no preview asset. */
1542
+ thumbnailHref: string | null;
1543
+ /** Asset role set across all assets on the item. */
1544
+ assetRoles: string[];
1545
+ /** Original item, retained so the inspector can show the raw JSON. */
1546
+ raw: StacItem;
666
1547
  }
667
- /** Single asset entry within a STAC item. */
668
- interface StacAsset {
669
- href: string;
670
- type?: string;
671
- title?: string;
672
- roles?: string[];
673
- /** Set when the asset carries `eo:bands`. */
674
- 'eo:bands'?: {
675
- name?: string;
676
- common_name?: string;
1548
+ /** Project a STAC Item into a `StacItemView`. Always succeeds. */
1549
+ declare function extractItemView(item: StacItem): StacItemView;
1550
+ /**
1551
+ * Numeric facet, e.g. cloud cover. `min`/`max` are derived from the loaded
1552
+ * views so the UI can use them as slider bounds. `count` is how many of the
1553
+ * input views had this field at all.
1554
+ */
1555
+ interface NumericFacet {
1556
+ kind: 'numeric';
1557
+ field: NumericFacetField;
1558
+ min: number;
1559
+ max: number;
1560
+ count: number;
1561
+ }
1562
+ /**
1563
+ * Enum facet, e.g. platform. `values` is sorted by descending count so the
1564
+ * most common values surface first in chip lists.
1565
+ */
1566
+ interface EnumFacet {
1567
+ kind: 'enum';
1568
+ field: EnumFacetField;
1569
+ values: {
1570
+ value: string;
1571
+ count: number;
677
1572
  }[];
678
1573
  }
1574
+ /**
1575
+ * Calendar-aligned bin granularity for the datetime histogram. Picked
1576
+ * automatically from the loaded items' time span so callers never see a
1577
+ * span-vs-resolution mismatch (e.g. month bins on a 30-day window or day bins
1578
+ * on a 20-year archive). See `pickGranularity` for the breakpoints.
1579
+ */
1580
+ type DatetimeGranularity = 'day' | 'week' | 'month' | 'year';
1581
+ /**
1582
+ * Datetime facet, with min/max for slider bounds and a calendar-aligned
1583
+ * histogram the UI can render under a range slider. Each bin spans one
1584
+ * `granularity` unit (UTC day, ISO week starting Monday, calendar month, or
1585
+ * calendar year). `bins[i]` is the count of items whose `datetime` falls
1586
+ * inside the bin starting at `binEdges[i]` (epoch ms, UTC). `bins.length ===
1587
+ * binEdges.length`, capped at `DATETIME_HISTOGRAM_BINS_MAX`.
1588
+ */
1589
+ interface DatetimeFacet {
1590
+ kind: 'datetime';
1591
+ field: 'datetime';
1592
+ /** Earliest datetime, ISO 8601. */
1593
+ min: string;
1594
+ /** Latest datetime, ISO 8601. */
1595
+ max: string;
1596
+ count: number;
1597
+ /** Per-bin counts. Length matches `binEdges.length`. */
1598
+ bins: number[];
1599
+ /** Auto-picked calendar granularity for each histogram bucket. */
1600
+ granularity: DatetimeGranularity;
1601
+ /** Epoch ms (UTC) of each bin's start boundary. Same length as `bins`. */
1602
+ binEdges: number[];
1603
+ }
1604
+ type Facet = NumericFacet | EnumFacet | DatetimeFacet;
1605
+ type NumericFacetField = 'cloudCover' | 'gsd';
1606
+ type EnumFacetField = 'collection' | 'platform' | 'constellation' | 'instruments' | 'assetRoles';
1607
+ /**
1608
+ * Soft cap on histogram bin count. The actual count is derived from the span
1609
+ * + granularity (e.g. a 5-year span at month granularity emits 60 bins, a
1610
+ * 90-day span at day granularity emits 90 bins). Spans that would exceed the
1611
+ * cap are clamped here, the next coarser granularity should already have been
1612
+ * picked by `pickGranularity` so this only protects against pathological
1613
+ * inputs.
1614
+ */
1615
+ declare const DATETIME_HISTOGRAM_BINS_MAX = 64;
1616
+ /** @deprecated Retained for backward compatibility. Use `DATETIME_HISTOGRAM_BINS_MAX`. */
1617
+ declare const DATETIME_HISTOGRAM_BINS = 32;
1618
+ /** Result of `buildFacets`: every facet that has variance in the input set. */
1619
+ interface FacetSet {
1620
+ datetime: DatetimeFacet | null;
1621
+ numeric: NumericFacet[];
1622
+ enums: EnumFacet[];
1623
+ /** Total number of views the facet set was built from. */
1624
+ total: number;
1625
+ }
1626
+ /**
1627
+ * Pick a calendar granularity from the time span. Inspired by lazycogs's
1628
+ * `_TemporalGrouper` family: short windows surface daily / weekly cadence,
1629
+ * long archives roll up to month / year so each bin still represents a
1630
+ * meaningful slice of data.
1631
+ */
1632
+ declare function pickGranularity(spanMs: number): DatetimeGranularity;
1633
+ /**
1634
+ * Scan a list of views and emit only those facets that have meaningful
1635
+ * variance. A facet is omitted when:
1636
+ * - numeric: fewer than two distinct finite values
1637
+ * - enum: fewer than two distinct values
1638
+ * - datetime: fewer than two parseable timestamps with distinct values
1639
+ *
1640
+ * The intent is "render only the controls that will narrow this dataset",
1641
+ * so callers can map each returned facet to a UI component without further
1642
+ * checks.
1643
+ */
1644
+ declare function buildFacets(views: StacItemView[]): FacetSet;
1645
+ /**
1646
+ * Mutable filter state, intended to be held by the UI layer. Each entry is
1647
+ * optional, omitting an entry means "no filter on this field". Numeric
1648
+ * ranges are inclusive on both ends. Enum sets are union-match (any value
1649
+ * in the set passes).
1650
+ */
1651
+ interface FacetState {
1652
+ datetime?: {
1653
+ min?: string;
1654
+ max?: string;
1655
+ };
1656
+ numeric?: Partial<Record<NumericFacetField, {
1657
+ min?: number;
1658
+ max?: number;
1659
+ }>>;
1660
+ enums?: Partial<Record<EnumFacetField, string[]>>;
1661
+ }
1662
+ /**
1663
+ * Filter views by `state`. Empty / missing entries are no-ops. Returns a new
1664
+ * array, the input is never mutated. Order is preserved, run `sortViews`
1665
+ * afterwards if a different order is needed.
1666
+ */
1667
+ declare function applyFacets(views: StacItemView[], state: FacetState | null | undefined): StacItemView[];
1668
+ type FacetSort = 'datetime-desc' | 'datetime-asc' | 'cloud-asc' | 'cloud-desc' | 'gsd-asc' | 'gsd-desc' | 'id-asc';
1669
+ /**
1670
+ * Sort views by one of a fixed set of strategies. Items missing the sort
1671
+ * field always sink to the bottom (regardless of asc/desc) so a `cloud-asc`
1672
+ * sort never surfaces "items with no cloud cover" above the cleanest scenes.
1673
+ */
1674
+ declare function sortViews(views: StacItemView[], sort: FacetSort): StacItemView[];
1675
+ /** True when any filter in `state` would actually narrow the input. */
1676
+ declare function hasActiveFilters(state: FacetState | null | undefined): boolean;
1677
+ /** Return `state` with every filter cleared. Useful for reset buttons. */
1678
+ declare function emptyFacetState(): FacetState;
679
1679
 
680
1680
  /**
681
1681
  * stac-geoparquet helpers.
@@ -769,69 +1769,449 @@ declare function pickStacPrimaryAsset(assets: Record<string, StacAsset> | null |
769
1769
  declare function stacRowToItem(row: StacGeoparquetRow, baseUrl: string, opts?: StacRowToItemOptions): StacItem;
770
1770
 
771
1771
  /**
772
- * Universal cloud storage URL / bucket parser.
1772
+ * STAC link-following hydrator. Walks `links[rel=item]` (Collection),
1773
+ * `links[rel=child]` → `links[rel=item]` (Catalog), and `links[rel=next]`
1774
+ * (paginated FeatureCollection / STAC API) into a flat list of StacItems.
1775
+ */
1776
+
1777
+ interface HydrateOptions {
1778
+ signal: AbortSignal;
1779
+ /** Max parallel fetches. Default 12. */
1780
+ concurrency?: number;
1781
+ /** Hard cap on items; catalogs larger than this are truncated. Default 2000. */
1782
+ limit?: number;
1783
+ /** Follow `links[rel=next]` pagination in FeatureCollections. Default true. */
1784
+ followPagination?: boolean;
1785
+ /** Emit fetched items in batches for progressive rendering. */
1786
+ onBatch?: (items: StacItem[]) => void;
1787
+ /** Emit progress totals for UI. */
1788
+ onProgress?: (fetched: number, totalHinted: number | undefined) => void;
1789
+ /**
1790
+ * Map an absolute HTTPS URL to a bucket-relative key when it belongs to the
1791
+ * caller's connection. When provided and it returns a non-null string,
1792
+ * `fetchJson` routes through the storage adapter (which handles SigV4) instead
1793
+ * of a raw cross-origin `fetch`, so private-bucket catalogs can be walked.
1794
+ */
1795
+ urlToKey?: (absoluteUrl: string) => string | null;
1796
+ /**
1797
+ * Optional native STAC API filters appended to the `rel="items"` endpoint
1798
+ * (and applied to `links[rel=next]` pages). Lets callers narrow a
1799
+ * collection by spatial / temporal extent before hydration.
1800
+ */
1801
+ itemsQuery?: StacItemsQuery;
1802
+ }
1803
+ /** Native filters supported by OGC API Features / STAC API on `/items`. */
1804
+ interface StacItemsQuery {
1805
+ /** WGS84 bbox `[west, south, east, north]`. */
1806
+ bbox?: [number, number, number, number];
1807
+ /** RFC 3339 instant or interval `start/end` (use `..` for open ends). */
1808
+ datetime?: string;
1809
+ /** Per-page item count hint, the server may cap this. */
1810
+ limit?: number;
1811
+ /**
1812
+ * CQL2-JSON filter expression (STAC API Filter extension). When set, gets
1813
+ * appended as `?filter=<json>&filter-lang=cql2-json` and re-stamped onto
1814
+ * every `rel="next"` page so cursor URLs cannot strip it.
1815
+ */
1816
+ filter?: unknown;
1817
+ }
1818
+ interface HydrateResult {
1819
+ items: StacItem[];
1820
+ truncated: boolean;
1821
+ rootBaseHref: string;
1822
+ }
1823
+ declare function hydrateStacItems(root: StacRoutableKind, baseHref: string, adapter: StorageAdapter, opts: HydrateOptions): Promise<HydrateResult>;
1824
+ /**
1825
+ * True when the payload exposes a `rel="items"` link (OGC API Features /
1826
+ * STAC API convention). Lets callers switch to viewport-scoped fetching
1827
+ * instead of walking every page.
1828
+ */
1829
+ declare function hasStacItemsEndpoint(payload: StacCollection | StacCatalog): boolean;
1830
+ /**
1831
+ * Resolve a possibly-relative href against a base. STAC catalogs commonly use
1832
+ * `./child/foo.json` or `../foo.json`. `new URL(relative, base)` handles both.
1833
+ */
1834
+ declare function absolutizeHref(href: string, baseHref: string): string;
1835
+
1836
+ /**
1837
+ * Translate a `FacetState` into native STAC API parameters and CQL2 filters,
1838
+ * gated by what the endpoint advertises in `conformsTo`. Pure TS, framework
1839
+ * and transport agnostic.
773
1840
  *
774
- * Accepts the many URI/URL formats that users commonly paste and extracts
775
- * the correct bucket, region, endpoint, and provider.
1841
+ * The split between this module and `stac-facets.ts` is intentional. Facets
1842
+ * own discovery and client-side filtering (works for any source). This
1843
+ * module owns *server-side push-down*, which only makes sense when the
1844
+ * source is a STAC API that supports OGC API Features query params or the
1845
+ * STAC API filter extension.
776
1846
  *
777
- * Supported URI schemes:
778
- * s3:// s3a:// s3n:// aws:// — Amazon S3 / S3-compatible
779
- * r2:// — Cloudflare R2
780
- * gs:// gcs:// — Google Cloud Storage
781
- * azure:// az:// — Azure Blob Storage
782
- * abfs:// abfss:// — Azure Data Lake (ADLS Gen2)
783
- * wasbs:// — Azure Blob (Hadoop WASB driver)
784
- * swift:// — OpenStack Swift
785
- * file:// filesystem:// — Local filesystem
1847
+ * Callers that want push-down:
1848
+ * 1. Fetch the API root, read `conformsTo`.
1849
+ * 2. `caps = sniffApiCapabilities(conformsTo)` once per session.
1850
+ * 3. On each pan / filter change:
1851
+ * const native = toNativeQuery(state, caps);
1852
+ * const filter = caps.cql2 ? toCql2Filter(state, caps) : null;
1853
+ * Pass `native` to the items endpoint and apply `filter` via
1854
+ * `?filter=<json-encoded>` when present. Anything that could not be
1855
+ * pushed down stays in `state` and is filtered client-side via
1856
+ * `applyFacets`.
1857
+ */
1858
+
1859
+ /**
1860
+ * Subset of STAC API capabilities relevant to filter push-down. Read once
1861
+ * per session from the API root's `conformsTo` array.
1862
+ */
1863
+ interface StacApiCapabilities {
1864
+ /** Supports `bbox=` query param (OGC API Features core). */
1865
+ bbox: boolean;
1866
+ /** Supports `datetime=` query param (OGC API Features core). */
1867
+ datetime: boolean;
1868
+ /** Supports `collections=` filter (STAC API Item Search). */
1869
+ collections: boolean;
1870
+ /** Supports the STAC API Filter extension via `filter=` + `filter-lang=cql2-json`. */
1871
+ cql2: boolean;
1872
+ /** Queryables endpoint advertised, lets clients sniff filterable property names. */
1873
+ queryables: boolean;
1874
+ }
1875
+ /**
1876
+ * Parse a STAC API `conformsTo` array into a capability flag set. Tolerant
1877
+ * of unknown URIs, missing entries, and casing differences. Defaults to all
1878
+ * `false` when given an empty / non-array input, so a caller that hasn't
1879
+ * fetched the root yet never accidentally pushes down something the API
1880
+ * cannot honor.
1881
+ */
1882
+ declare function sniffApiCapabilities(conformsTo: unknown): StacApiCapabilities;
1883
+ /**
1884
+ * Generic STAC items query, compatible with both OGC API Features
1885
+ * (`/collections/{id}/items`) and STAC API Item Search (`/search`). Mirrors
1886
+ * the shape `stac-hydrate.ts::StacItemsQuery` expects, plus optional
1887
+ * `collections` and `filter` for the search endpoint.
1888
+ */
1889
+ interface StacNativeQuery {
1890
+ bbox?: [number, number, number, number];
1891
+ datetime?: string;
1892
+ collections?: string[];
1893
+ limit?: number;
1894
+ /** CQL2-JSON object, encode with `JSON.stringify` when serializing. */
1895
+ filter?: unknown;
1896
+ 'filter-lang'?: 'cql2-json';
1897
+ }
1898
+ interface ToNativeQueryOptions {
1899
+ bbox?: [number, number, number, number];
1900
+ limit?: number;
1901
+ }
1902
+ /**
1903
+ * Translate `state` into the subset of native query params the API supports.
1904
+ * Anything that can't be pushed down is silently dropped here, the caller is
1905
+ * expected to keep applying it client-side via `applyFacets`. This is safe
1906
+ * because client filtering is always a superset, never a contradiction.
786
1907
  *
787
- * Supported HTTPS URL patterns:
788
- * https://<bucket>.s3.<region>.amazonaws.com[/prefix] — AWS virtual-hosted
789
- * https://s3.<region>.amazonaws.com/<bucket>[/prefix] — AWS path-style
790
- * https://s3.amazonaws.com/<bucket> — AWS global
791
- * https://<account>.r2.cloudflarestorage.com/<bucket> — Cloudflare R2
792
- * https://storage.googleapis.com/<bucket> — Google Cloud Storage
793
- * https://<bucket>.storage.googleapis.com[/prefix] — GCS virtual-hosted
794
- * https://<bucket>.<region>.digitaloceanspaces.com — DigitalOcean Spaces
795
- * https://<region>.digitaloceanspaces.com/<bucket> — DO Spaces path-style
796
- * https://s3.<region>.wasabisys.com/<bucket> — Wasabi
797
- * https://f<id>.backblazeb2.com/file/<bucket> — Backblaze B2
798
- * https://<bucket>.s3.<region>.backblazeb2.com — B2 S3-compatible
799
- * https://<bucket>.oss-<region>.aliyuncs.com — Alibaba Cloud OSS
800
- * https://<bucket>.cos.<region>.myqcloud.com — Tencent COS
801
- * https://storage.yandexcloud.net/<bucket> — Yandex Cloud
802
- * https://gateway.storjshare.io/<bucket> — Storj S3 gateway
803
- * https://link.storjshare.io/raw/<access>/<bucket> — Storj linksharing
804
- * https://<custom-endpoint>/<bucket> — Generic S3-compatible
1908
+ * `bbox` / `limit` are accepted as overrides because they typically come
1909
+ * from the viewer's viewport + user setting, not from `state`.
1910
+ */
1911
+ declare function toNativeQuery(state: FacetState | null | undefined, caps: StacApiCapabilities, opts?: ToNativeQueryOptions): StacNativeQuery;
1912
+ /**
1913
+ * CQL2-JSON expression node (very loose typing because the spec allows
1914
+ * arbitrary nesting and we only emit a small subset). Use `unknown` at the
1915
+ * boundary, cast inside this module.
1916
+ */
1917
+ type Cql2Node = unknown;
1918
+ /**
1919
+ * Build a CQL2-JSON `and` expression from a `FacetState`, covering the
1920
+ * filters that aren't already handled by native params. Returns `null` when
1921
+ * nothing in `state` requires CQL2 (so the caller can omit `filter=`).
805
1922
  *
806
- * Also handles plain bucket names (no protocol).
1923
+ * Currently emits:
1924
+ * - eo:cloud_cover (between)
1925
+ * - gsd (between)
1926
+ * - proj:epsg (=)
1927
+ * - platform (in)
1928
+ * - constellation (in)
1929
+ * - instruments (a_overlaps)
1930
+ *
1931
+ * `collection` and `datetime` are skipped here when the corresponding native
1932
+ * cap is set, since those are cheaper to push as plain query params. They
1933
+ * fall through to CQL2 only when the API advertises CQL2 but not the
1934
+ * matching native capability (rare but legal).
807
1935
  */
808
- type StorageProvider = string;
809
- interface ParsedStorageUrl {
810
- bucket: string;
811
- region: string;
812
- endpoint: string;
813
- provider: StorageProvider;
814
- /** Original prefix/path after bucket, if any */
815
- prefix: string;
1936
+ declare function toCql2Filter(state: FacetState | null | undefined, caps: StacApiCapabilities): Cql2Node | null;
1937
+ /**
1938
+ * Subtract everything that was pushed down from `state`, returning the
1939
+ * remaining state that the caller still has to apply client-side. Lets the
1940
+ * UI avoid double-filtering (which would just be a no-op but wastes work).
1941
+ *
1942
+ * This is a structural diff, not a deep clone, the input is not mutated.
1943
+ */
1944
+ declare function residualState(state: FacetState | null | undefined, caps: StacApiCapabilities): FacetState;
1945
+
1946
+ /**
1947
+ * StacSource contract. Unified interface for the three STAC ingestion paths
1948
+ * (STAC API, stac-geoparquet, self-contained static catalog) so the viewer
1949
+ * has a single orchestration loop and the UI can branch on capability flags
1950
+ * instead of hard-coded discovery modes.
1951
+ *
1952
+ * Pure TypeScript. No Svelte / maplibre / deck.gl / DuckDB on this import
1953
+ * graph. The DuckDB-bound parquet implementation lives under `query/` so the
1954
+ * `utils/` side stays publishable via `@walkthru-earth/objex-utils` (slice 6).
1955
+ *
1956
+ * Per-batch `pushedDown` / `residual` reporting lets the caller skip
1957
+ * client-side filtering for dimensions the engine already narrowed, and lets
1958
+ * the UI render capability badges. A parquet file with a STRUCT
1959
+ * `properties` column can push `eo:cloud_cover` while a sibling file with
1960
+ * an opaque `properties` cannot, so the report is per batch, not per source.
1961
+ */
1962
+
1963
+ /** Which underlying engine drives this source. Used by the viewer to pick
1964
+ * atomic-swap-vs-append, by the UI to choose copy / badges, and by tests. */
1965
+ type StacSourceKind = 'api' | 'parquet' | 'static';
1966
+ /**
1967
+ * Per-source capability surface. Read by the viewer at construction (no await
1968
+ * — sources are synchronous to construct so the orchestrator can branch on
1969
+ * `kind` before any I/O) and by the filter UI to decide which controls to
1970
+ * disable / badge as "client-side only".
1971
+ *
1972
+ * The `pushdown` map is exhaustive: every facet field listed in
1973
+ * `FacetState` has a flag here, so adding a new facet is a compile-time
1974
+ * error in every consumer until they handle it.
1975
+ */
1976
+ interface StacSourceCapabilities {
1977
+ kind: StacSourceKind;
1978
+ /** Human-readable label for HUD copy. e.g. "STAC API", "stac-geoparquet". */
1979
+ label: string;
1980
+ /** True when count(filter, bbox) is cheap. UI surfaces "Y of X". */
1981
+ countAvailable: boolean;
1982
+ /** True when query() yields multiple batches before completing. */
1983
+ streaming: boolean;
1984
+ /**
1985
+ * True when the underlying source is a hive-partitioned parquet directory
1986
+ * (e.g. `s3://bucket/prefix/year=2023/month=01/...`). Set by the parquet
1987
+ * source when the factory detects a directory layout (or the SDK passes
1988
+ * `hivePartitioned: true`). Lets the viewer surface a HUD hint without
1989
+ * inspecting `kind === 'parquet'` alone, since the same `kind` covers
1990
+ * single-file stac-geoparquet.
1991
+ */
1992
+ hivePartitioned?: boolean;
1993
+ pushdown: {
1994
+ bbox: boolean;
1995
+ datetime: boolean;
1996
+ collection: boolean;
1997
+ cloudCover: boolean;
1998
+ gsd: boolean;
1999
+ epsg: boolean;
2000
+ platform: boolean;
2001
+ constellation: boolean;
2002
+ instruments: boolean;
2003
+ assetRoles: boolean;
2004
+ };
816
2005
  }
817
- interface Defaults {
818
- region?: string;
819
- endpoint?: string;
820
- provider?: StorageProvider;
2006
+ /** Per-query inputs. The signal is required, sources MUST throw
2007
+ * `DOMException("Aborted", "AbortError")` on abort, never silently complete. */
2008
+ interface StacSourceRequest {
2009
+ /** WGS84 viewport bbox `[west, south, east, north]`. Required. Sources that
2010
+ * cannot push down bbox still receive it so they can stream the whole set
2011
+ * and rely on the caller's residual filter. */
2012
+ bbox: [number, number, number, number];
2013
+ filter: FacetState;
2014
+ limit: number;
2015
+ /** Per-page hint for sources that paginate. Server may ignore. */
2016
+ pageSize?: number;
2017
+ signal: AbortSignal;
2018
+ }
2019
+ /** One yielded batch of items. */
2020
+ interface StacSourceBatch {
2021
+ items: StacItem[];
2022
+ /** Subset of filter the source / engine applied. UI reports as "pushed". */
2023
+ pushedDown: FacetState;
2024
+ /** Subset of filter the caller still has to apply via applyFacets(). */
2025
+ residual: FacetState;
2026
+ /** True when no more batches will arrive for this request. The async
2027
+ * iterator's own end-of-iteration also signals done; this flag lets a
2028
+ * caller break the loop at the moment a single-yield source completes. */
2029
+ done: boolean;
2030
+ /** Best-effort hint of total matching items, when the source knows. */
2031
+ totalHinted?: number;
821
2032
  }
2033
+ interface StacSource {
2034
+ capabilities: StacSourceCapabilities;
2035
+ query(req: StacSourceRequest): AsyncIterable<StacSourceBatch>;
2036
+ /** Optional cheap count(filter, bbox). Surfaced as "Y of X" when set. */
2037
+ count?(filter: FacetState, bbox: StacSourceRequest['bbox'], signal: AbortSignal): Promise<number>;
2038
+ }
2039
+ /** All-false push-down flags. Helper to keep capability declarations terse. */
2040
+ declare function emptyPushdown(): StacSourceCapabilities['pushdown'];
2041
+
822
2042
  /**
823
- * Parse a user-provided bucket/URL string into structured storage connection parts.
2043
+ * STAC API implementation of the StacSource contract. Wraps `hydrateStacItems`
2044
+ * link-walking with `itemsQuery: {bbox, datetime, limit, filter}` push-down and
2045
+ * yields each `onBatch` as a `StacSourceBatch`.
2046
+ *
2047
+ * Slice 2 sniffs the catalog/collection's `conformsTo` array once per source
2048
+ * instance, builds a CQL2-JSON filter (cloud cover / gsd / platform /
2049
+ * constellation / instruments / collection) via `toCql2Filter`, and reports
2050
+ * the actually-pushed subset of `FacetState` plus the residual the caller still
2051
+ * has to apply via `applyFacets`. When the sniff fails or `conformsTo` lacks
2052
+ * the Filter extension, behavior degrades gracefully to slice-1 (bbox+datetime
2053
+ * only).
2054
+ *
2055
+ * Pure TypeScript. No DuckDB / Svelte / maplibre / deck.gl import. The
2056
+ * `StorageAdapter` import is structural (an interface), and the actual
2057
+ * adapter is injected via `deps`.
824
2058
  */
825
- declare function parseStorageUrl(input: string, defaults?: Defaults): ParsedStorageUrl;
2059
+
2060
+ interface StacApiSourceDeps {
2061
+ adapter: StorageAdapter;
2062
+ baseHref: string;
2063
+ urlToKey?: (absoluteUrl: string) => string | null;
2064
+ concurrency?: number;
2065
+ }
826
2066
  /**
827
- * Returns true if the input looks like a URL/URI rather than a plain bucket name.
828
- * Covers all recognized cloud storage URI schemes.
2067
+ * Construct a STAC API source. `kind` is the classified payload from
2068
+ * `classifyStac` (Collection / Catalog with `rel="items"`, or a STAC API
2069
+ * `item-collection` page). The factory checks before dispatching here, this
2070
+ * function does not re-validate.
2071
+ *
2072
+ * The advertised `capabilities.pushdown` flags reflect the *ceiling* of what a
2073
+ * STAC API can push (everything CQL2 covers). The actual push-down per request
2074
+ * depends on the `conformsTo` sniff and is reported per-batch in
2075
+ * `pushedDown` / `residual` so the caller can re-filter only what's left.
829
2076
  */
830
- declare function looksLikeUrl(input: string): boolean;
2077
+ declare function createApiSource(kind: StacRoutableKind, deps: StacApiSourceDeps): StacSource;
2078
+
831
2079
  /**
832
- * Given a parsed URL result, build a human-readable summary of what was detected.
2080
+ * Self-contained static catalog implementation of the StacSource contract.
2081
+ * Wraps `hydrateStacItems` link-walking with no `itemsQuery`, so the entire
2082
+ * advertised tree is fetched and the caller filters client-side.
2083
+ *
2084
+ * Slice 1 reports zero push-down. Slice 4 adds extent-pruning (skip child
2085
+ * links whose `extent.spatial` / `extent.temporal` does not intersect the
2086
+ * request bbox / datetime), which lifts `bbox` and `datetime` to true.
2087
+ *
2088
+ * Pure TypeScript. No DuckDB / Svelte / maplibre / deck.gl import.
833
2089
  */
834
- declare function describeParseResult(parsed: ParsedStorageUrl): string;
2090
+
2091
+ interface StacStaticSourceDeps {
2092
+ adapter: StorageAdapter;
2093
+ baseHref: string;
2094
+ urlToKey?: (absoluteUrl: string) => string | null;
2095
+ concurrency?: number;
2096
+ }
2097
+ declare function createStaticSource(kind: StacRoutableKind, deps: StacStaticSourceDeps): StacSource;
2098
+
2099
+ /**
2100
+ * STAC Storage Extension parser.
2101
+ *
2102
+ * Detects the Storage Extension version on a STAC Item and extracts
2103
+ * connection-relevant hints (region, requester-pays, custom-S3 endpoint).
2104
+ *
2105
+ * Inspired by lazycogs's `_storage_ext.py`. Pure TypeScript, no fetch, no
2106
+ * Svelte dependency. Suitable for `@walkthru-earth/objex-utils` re-export.
2107
+ *
2108
+ * Supported schema URLs:
2109
+ * - https://stac-extensions.github.io/storage/v1.0.0/schema.json
2110
+ * - https://stac-extensions.github.io/storage/v2.0.0/schema.json
2111
+ *
2112
+ * v1 (item / asset properties)
2113
+ * storage:platform e.g. "AWS", "GCP", "AZURE"
2114
+ * storage:region
2115
+ * storage:requester_pays boolean
2116
+ * storage:tier (ignored — no obstore equivalent)
2117
+ *
2118
+ * v2 (item-level scheme map + asset-level refs)
2119
+ * properties.storage:schemes = {
2120
+ * <ref>: { type, platform, region?, requester_pays?, endpoint? }
2121
+ * }
2122
+ * asset.storage:refs = ["primary", ...] (first matching ref wins)
2123
+ *
2124
+ * Asset-level fields take precedence over item-level fields in v1.
2125
+ */
2126
+
2127
+ /** Recognized Storage Extension schema versions. */
2128
+ type StorageExtensionVersion = '1.0.0' | '2.0.0';
2129
+ /**
2130
+ * Connection-relevant hints extracted from the Storage Extension. All fields
2131
+ * are nullable so callers can merge selectively into existing config without
2132
+ * clobbering user-set values.
2133
+ */
2134
+ interface StorageHints {
2135
+ /** e.g. "AWS", "GCP", "AZURE", "MINIO". Uppercased. Null when absent. */
2136
+ platform: string | null;
2137
+ /** Region code, e.g. "us-west-2". Null when absent. */
2138
+ region: string | null;
2139
+ /** True when requester-pays must be set. False when absent or false. */
2140
+ requesterPays: boolean;
2141
+ /** Concrete S3-compatible endpoint URL. Null unless v2 `custom-s3` with
2142
+ * a non-templated `platform` value. */
2143
+ endpoint: string | null;
2144
+ }
2145
+ /** Empty hints record. Returned when extension absent or unparseable. */
2146
+ declare function emptyStorageHints(): StorageHints;
2147
+ /**
2148
+ * Scan `item.stac_extensions[]` for the Storage Extension schema URL and
2149
+ * return its parsed version. Returns null when the extension is absent or
2150
+ * the version is not one we recognize.
2151
+ */
2152
+ declare function detectStorageExtensionVersion(item: StacItem): StorageExtensionVersion | null;
2153
+ /**
2154
+ * Extract connection hints from a STAC Item. Dispatches on the detected
2155
+ * Storage Extension version. Returns `emptyStorageHints()` when the
2156
+ * extension is absent or fails to parse.
2157
+ *
2158
+ * `assetKey` is optional. When given:
2159
+ * - v1: that asset's overrides take precedence over item-level fields.
2160
+ * - v2: that asset's `storage:refs[0]` resolves the item scheme.
2161
+ * When omitted in v2, the first scheme found in any asset's refs wins.
2162
+ */
2163
+ declare function extractStorageHints(item: StacItem, assetKey?: string): StorageHints;
2164
+ /**
2165
+ * TODO(host-detection): a future PR should call this from the connection
2166
+ * auto-fill path so a STAC Item carrying Storage Extension metadata can
2167
+ * pre-populate `region` and (for `custom-s3`) `endpoint` on a new
2168
+ * connection.
2169
+ *
2170
+ * Today this lives next to the parser so consumers can opt-in without
2171
+ * touching `host-detection.ts` or the connection store. The function is
2172
+ * intentionally generic — it takes any object with `region` / `endpoint`
2173
+ * keys and returns a shallow copy with hint fields filled only when the
2174
+ * existing value is empty.
2175
+ */
2176
+ declare function applyStorageHintsToConnection<T extends {
2177
+ region?: string;
2178
+ endpoint?: string;
2179
+ }>(conn: T, hints: StorageHints): T;
2180
+
2181
+ /**
2182
+ * Open-time storage probe. Issues a single ranged GET against a presigned
2183
+ * asset URL to surface auth, CORS, and bucket-misconfiguration errors at
2184
+ * viewer load time, before any tile read kicks off.
2185
+ *
2186
+ * Inspired by lazycogs `_smoketest_store` (developmentseed/lazycogs). The
2187
+ * Python library calls `store.head()` on a representative asset during
2188
+ * `open()` so credential or region misconfiguration fails in <1s instead of
2189
+ * mid-mosaic when the first COG range fetch errors out.
2190
+ *
2191
+ * We use ranged GET instead of HEAD because:
2192
+ * - Many private buckets allow GET but block HEAD via CORS.
2193
+ * - Range `bytes=0-0` is one byte, cheaper than a full body fetch.
2194
+ * - Successful 206 / 200 confirms BOTH auth and CORS headers are correct,
2195
+ * which a HEAD response sometimes lies about under bucket policies that
2196
+ * return mismatched `Access-Control-Expose-Headers`.
2197
+ *
2198
+ * Pure TS, no Svelte / framework deps. Safe to publish via objex-utils.
2199
+ */
2200
+ type SmokeTestResult = {
2201
+ ok: true;
2202
+ } | {
2203
+ ok: false;
2204
+ status: number | null;
2205
+ reason: string;
2206
+ };
2207
+ /**
2208
+ * Probe a presigned URL with a one-byte Range GET. Resolves to `{ ok: true }`
2209
+ * on 200 / 206, otherwise returns a structured failure with the HTTP status
2210
+ * (or null when the request never reached a server, e.g. CORS preflight
2211
+ * failure or DNS error). AbortError is re-thrown so callers can distinguish
2212
+ * intentional cancellation from real failures.
2213
+ */
2214
+ declare function smokeTestHref(href: string, signal?: AbortSignal): Promise<SmokeTestResult>;
835
2215
 
836
2216
  /**
837
2217
  * Lightweight WKB (Well-Known Binary) parser for extracting coordinates.
@@ -877,4 +2257,4 @@ declare function findGeoColumnFromRows(rows: Record<string, unknown>[], schema:
877
2257
  type: string;
878
2258
  }[]): string | null;
879
2259
 
880
- export { type AccessMode, type AccessModeInput, COPY_FEEDBACK_MS, type CogInfo, type Connection, type ConnectionConfig, DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, type Defaults, type DuckDbReadFn, type FileCategory, type FileEntry, type FileTypeInfo, type GeoArrowGeomType, type GeoArrowResult, type GeoBounds, type GeoColumnMeta, type GeoParquetMeta, type GeoType, type HexRow, LAYER_HUE_MULTIPLIER, type ListPage, MAX_QUERY_HISTORY_ENTRIES, type MapQueryHandle, type MapQueryResult, PROVIDERS, PROVIDER_IDS, type ParquetFileMetadata, type ParsedGeometry, type ParsedMarkdownDocument, type ParsedStorageUrl, type ProviderDef, type ProviderId, type ProviderRegion, QueryCancelledError, type QueryEngine, type QueryHandle, type QueryResult, type QuerySource, SF_LABELS, SQL_PREVIEW_LENGTH, STAC_GEOPARQUET_REQUIRED_COLUMNS, STORAGE_KEYS, type SchemaField, type SortConfig, type SortDirection, type SortField, type SqlBlock, type StacBboxStruct, type StacGeoparquetRow, type StacGeoparquetSchemaColumn, type StacRowToItemOptions, type StorageAdapter, type StorageProvider, type Tab, type Theme, type TypeCategory, UrlAdapter, VIEWER_DIR_EXTENSIONS, type ViewerKind, WGS84_CODES, type WriteResult, buildDataTypeLabel, buildDuckDbSource, buildEndpointFromTemplate, buildGeoArrowTables, buildProviderBaseUrl, clampBounds, classifyType, describeParseResult, escapeCsvField, exportToCsv, exportToJson, extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, findGeoColumn, findGeoColumnFromRows, flattenStacBbox, formatDate, formatFileSize, formatValue, generateHexDump, getAccessMode, getDuckDbReadFn, getFileExtension, getFileTypeInfo, getMimeType, getNativeScheme, getProvider, getViewerKind, handleLoadError, interpolateTemplates, isCloudNativeFormat, isGcsProvider, isPubliclyStreamable, isQueryable, isStacGeoparquetSchema, jsonReplacerBigInt, loadFromStorage, looksLikeUrl, markSqlBlocks, normalizeGeomType, parseMarkdownDocument, parseStorageUrl, parseWKB, persistToStorage, pickStacPrimaryAsset, readParquetMetadata, resolveCloudUrl, resolveProviderEndpoint, resolveStacAssetHref, safeClamp, safeDecodeURIComponent, serializeToCsv, serializeToJson, sortFileEntries, stacRowToItem, toBinary, toggleSortField, typeBadgeClass, typeColor, typeLabel };
2260
+ export { type AccessMode, type AccessModeInput, type AppConfig, type AppConfigDefaults, type AppConfigUi, type AttachPixelInspectorOptions, type BandMap, type BandSlot, type BasemapConfig, COPY_FEEDBACK_MS, type ChannelComposite, type ChannelRef, type CogAsset, type CogInfo, type Connection, type ConnectionConfig, type ConnectionIdentityInput, type ConnectionSeed, DATETIME_HISTOGRAM_BINS, DATETIME_HISTOGRAM_BINS_MAX, DEFAULT_APP_CONFIG, DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, type DatetimeFacet, type DatetimeGranularity, type Defaults, type DetectedHost, type DuckDbReadFn, type EnumFacet, type EnumFacetField, type Facet, type FacetSet, type FacetSort, type FacetState, type FileCategory, type FileEntry, type FileTypeInfo, type GeoArrowGeomType, type GeoArrowResult, type GeoBounds, type GeoColumnMeta, type GeoParquetMeta, type GeoType, type GeometryTypeInfo, type HexRow, type HydrateOptions, type HydrateResult, LAYER_HUE_MULTIPLIER, type ListPage, LruCache, type LruCacheOptions, MAX_QUERY_HISTORY_ENTRIES, type MapLike, type MapQueryHandle, type MapQueryResult, MarkdownSqlContext, type MosaicSourceMeta, type NotebookConfig, type NotebookMeta, type NumericFacet, type NumericFacetField, PRESETS, PROVIDERS, PROVIDER_IDS, type ParquetFileMetadata, type ParsedGeometry, type ParsedMarkdownDocument, type ParsedStorageUrl, type PixelInspectCallbacks, type PixelInspectClickEvent, type PixelInspectClickHandler, type PixelInspectProbe, type PixelInspectProbeRequest, type PresetDef, type ProviderDef, type ProviderId, type ProviderRegion, QueryCancelledError, type QueryEngine, type QueryHandle, type QueryResult, type QuerySource, type RasterBandAsset, SF_LABELS, SQL_PREVIEW_LENGTH, STAC_API_PATH_RE, STAC_COG_ASSET_KEYS, STAC_GEOPARQUET_REQUIRED_COLUMNS, STORAGE_KEYS, type SchemaField, type SmokeTestResult, type SortConfig, type SortDirection, type SortField, type SqlBlock, type StacApiCapabilities, type StacApiSourceDeps, type StacAsset, type StacBboxStruct, type StacCatalog, type StacCollection, type StacFeatureCollection, type StacGeoparquetRow, type StacGeoparquetSchemaColumn, type StacItem, type StacItemView, type StacItemsQuery, type StacLink, type StacNativeQuery, type StacRoutableKind, type StacRowToItemOptions, type StacSource, type StacSourceBatch, type StacSourceCapabilities, type StacSourceKind, type StacSourceRequest, type StacStaticSourceDeps, type StorageAdapter, type StorageExtensionVersion, type StorageHints, type StorageProvider, type Tab, type Theme, type ToNativeQueryOptions, type TypeCategory, UrlAdapter, type UrlClassification, VIEWER_DIR_EXTENSIONS, type ViewerKind, WGS84_CODES, type WriteResult, absolutizeHref, allChannelsBand0, applyFacets, applyPreset, applyStacItemStorageHints, applyStorageHintsToConnection, attachPixelInspector, availablePresets, buildDataTypeLabel, buildDuckDbSource, buildEndpointFromTemplate, buildFacets, buildGeoArrowTables, buildMosaicSourceMeta, buildProviderBaseUrl, buildTransformExpr, clampBounds, classifyStac, classifyType, classifyUrl, coerceBool, coercePositiveInt, coerceString, coerceTheme, compositeFromUrl, compositeToUrl, connectionIdentityKey, copyToClipboard, createApiSource, createStaticSource, describeParseResult, detectHostBucket, detectMosaicCapable, detectMultiCogCapable, detectStorageExtensionVersion, emptyFacetState, emptyPushdown, emptyStorageHints, escapeCsvField, exportToCsv, exportToJson, extractBounds, extractCogAssets, extractEpsgFromGeoMeta, extractGeometryTypes, extractItemView, extractMosaicAssets, extractRasterBandAssets, extractSentinelBandAssets, extractStorageHints, findGeoColumn, findGeoColumnFromRows, flattenStacBbox, formatDate, formatFileSize, formatValue, generateHexDump, getAccessMode, getDuckDbReadFn, getFileExtension, getFileTypeInfo, getMimeType, getNativeScheme, getProvider, getViewerKind, handleLoadError, hasActiveFilters, hasCompositableBands, hasRgbBands, hasStacItemsEndpoint, hydrateStacItems, interpolateTemplates, isAbortError, isCloudNativeFormat, isGcsProvider, isKnownBucketHost, isPubliclyStreamable, isQueryable, isSameConnectionIdentity, isSingleAssetComposite, isStacCatalog, isStacCollection, isStacFeatureCollection, isStacGeoparquetSchema, isStacItem, isWgs84Crs, jsonReplacerBigInt, loadFromStorage, looksLikeUrl, markSqlBlocks, mergeAppConfig, normalizeEndpoint, normalizeGeomType, normalizeProvider, parseGeometryTypeCrs, parseMarkdownDocument, parseStorageUrl, parseVisibilityParam, parseWKB, persistToStorage, pickCogAssetHref, pickGranularity, pickNaturalColorComposite, pickStacPrimaryAsset, presetMatchesComposite, readParquetMetadata, renderNotebook, residualState, resolveBandSlotAssetKey, resolveBasemap, resolveCloudUrl, resolvePresetComposite, resolveProviderEndpoint, resolveSetting, resolveStacAssetHref, safeClamp, safeDecodeURIComponent, serializeToCsv, serializeToJson, smokeTestHref, sniffApiCapabilities, sortFileEntries, sortViews, spatialCellKey, stacItemBbox, stacRowToItem, syntheticSelfAsset, toBinary, toCql2Filter, toNativeQuery, toggleSortField, typeBadgeClass, typeColor, typeLabel, wireCodeCopyButtons, wrapWkbWithCrs };