Direct COG inspection, bounded reads, and MapLibre rendering

@honua/sdk-js/cog is an experimental, browser-safe boundary from static STAC asset discovery to direct Cloud Optimized GeoTIFF inspection, pixel-window reads, and an opt-in MapLibre image-source renderer. It is intentionally a separate subpath: the stable root, /honua, and browser barrels do not import a GeoTIFF or MapLibre implementation.

Trust boundary

Start with the asset candidates produced by an explicit static-STAC connect() call. openStacCogAsset() accepts only a candidate whose state is classified, whose kind is cog, whose URL is credential-free HTTP(S), and whose COG classification is supported by declared or probed media-type evidence. A .tif suffix, a STAC extension URI, or an ambiguous plain image/tiff declaration cannot cross the boundary.

The caller injects a decoder implementing two methods, inspect() and readWindow(). The decoder receives a bounded readRange() callback for each operation; it never receives an unbounded Fetch function. This keeps a preferred GeoTIFF package lazy and application-owned while Honua enforces the network and lifecycle policy.

import { connect } from "@honua/sdk-js";
import { openStacCogAsset, type CogDecoderFactory } from "@honua/sdk-js/cog";

declare const decoderFactory: CogDecoderFactory; // adapt the optional decoder chosen by the app

const connection = await connect({
  endpoint: "https://data.example/catalog.json",
  protocol: "stac",
  authorizationScopeFingerprint: "anonymous",
});
const candidate = connection.inspection.stacStatic?.assetCandidates.find(
  (asset) => asset.state === "classified" && asset.kind === "cog",
);
if (!candidate) throw new Error("No evidence-classified COG asset was discovered.");

const asset = openStacCogAsset(candidate, { decoderFactory });
try {
  const metadata = await asset.inspect();
  const window = await asset.readWindow({
    x: 0,
    y: 0,
    width: Math.min(256, metadata.width),
    height: Math.min(256, metadata.height),
    bands: [metadata.bands[0].index],
  });
  console.log(metadata.crs, metadata.resolution, window.transfer);
} finally {
  await asset.dispose();
}

Bounded transport

Every asset request is a partial GET with an exact Range header. Redirects and credentials are disabled. A readable 206 response must expose the exact Content-Range; 200, 416, opaque CORS responses, compressed ranges, dishonest lengths, changed validators, stream overflow, and a range covering the complete resource fail with a typed HonuaCogError. Response bodies are cancelled on rejection instead of falling back to whole-file materialization.

Default metadata, window, per-range, total-transfer, pixel, and decoded-byte ceilings are available as DEFAULT_COG_TRANSFER_LIMITS. Callers may lower or raise them only within hard SDK ceilings. inspection.transfer, window.transfer, and asset.transfer() expose a deterministic request-order ledger with byte counts and outcomes; it contains no wall-clock fields.

Metadata and lifecycle

Inspection normalizes dimensions, CRS, bands, nodata, resolution, footprint, overview decimations, STAC evidence, document provenance, and a stable asset validator. A decoder reporting a non-COG format, unsupported CRS, malformed footprint, or unbounded metadata is rejected.

Window reads use pixel coordinates and one-based inspected band indices. A new window read aborts the preceding one and the preceding promise rejects with obsolete-read. Caller aborts reject with aborted; disposal rejects pending work with disposed and closes even a decoder factory that settles late.

An optional sampling descriptor asks the decoder for a bounded output size, nearest or bilinear resampling, and one exact overview decimation advertised by inspection. The session validates output dimensions, decoded pixels, and decoded bytes. Existing requests without sampling remain native-resolution reads.

Viewport-driven MapLibre image source

mountStacCogAssetToMapLibre() accepts only the S1 session and a structurally typed caller-owned MapLibre map. It imports neither maplibre-gl nor a raster decoder. The mount inspects the asset, intersects the current getBounds() viewport, chooses an advertised overview from the canvas/zoom target, reads a bounded window, alpha-masks numeric nodata, encodes a bounded PNG with browser Canvas 2D, and adds or updates one native MapLibre image source.

import {
  mountStacCogAssetToMapLibre,
  openStacCogAsset,
  type CogDecoderFactory,
  type StacCogAssetToMapLibreMap,
} from "@honua/sdk-js/cog";

declare const candidate: Parameters<typeof openStacCogAsset>[0];
declare const decoderFactory: CogDecoderFactory;
declare const map: StacCogAssetToMapLibreMap; // a maplibregl.Map satisfies this shape

const session = openStacCogAsset(candidate, { decoderFactory });
const mounted = mountStacCogAssetToMapLibre(map, session, {
  sourceId: "observed-imagery",
  beforeId: "labels",
  paint: { "raster-opacity": 0.85 },
});

try {
  const readiness = await mounted.ready;
  console.log(readiness.state, readiness.lastRender?.window, readiness.diagnostics);
} finally {
  await mounted.dispose(); // removes listeners/layer/source and disposes the session
}

moveend (including zoom) and resize start a new refresh. The newest generation wins; stale reads and even a completed stale encode are checked again immediately before renderer mutation. ready and refresh() settle on supersession or disposal instead of leaving pending promises. Readiness means the image-source mutation was accepted; consumers that need a visually painted frame should separately wait for the caller-owned map's render/idle event.

The direct renderer deliberately supports only north-up, single-polygon grids whose dimensions, resolution, and extent agree; EPSG:4326, OGC:CRS84, or EPSG:3857; and uint8 identity-scaled grayscale or exact/explicit RGB(A) bands. Rotated/multipart grids, other CRSs, string or partial nodata, non-uint8 data, wrapped viewports, canvas/PNG failures, and source identity drift fail visibly without projection, stretching, or analytic fallback. A viewport outside the asset and an overview/output/encoded-size overflow return deterministic outside-extent or refused readiness without a window read or map mutation.

DEFAULT_COG_MAPLIBRE_RENDER_LIMITS publishes the default output-pixel, overview-source-pixel, encoded-byte, canvas-dimension, and diagnostic-history ceilings. Callers may tighten them or raise them only within hard SDK limits. This S2 bridge does not select assets in UI, cache decoded pixels, perform raster analytics, or supply a GeoTIFF implementation.