# TypeScript Frontend Integration Guide This document explains how frontend developers should integrate with the backend through the reusable TypeScript client at `libs/geo-api-client`. > **See also:** [Frontend Development](frontend-development.md) — demo app (`web/`), local dev, build steps. > > **Asset docs:** [Assets Storage and Sharing](assets-storage-and-sharing.md) and [Docker MinIO Local Development](docker-minio-local-dev.md). Primary backend URL for integration: - `https://tenerife.baby/` Deployment: API is proxied via reverse proxy from `https://tenerife.baby` to backend at `172.17.0.1:8122`. Docker Compose maps port 8122 for the reverse proxy. ## Goals - Keep cryptographic signing logic in one place. - Avoid duplicating API request code in frontend apps. - Use a consistent local key storage format across projects. ## Client package location - Source: `libs/geo-api-client/src` - Entry point: `libs/geo-api-client/src/index.ts` - Build output (browser ESM): `libs/geo-api-client/dist/index.js` ## Build and test the client ```bash cd libs/geo-api-client bun install bun test # unit + integration tests (docs flow) bun run build ``` Integration tests in `test/integration.test.ts` cover both: - Base flow: register, login, collection and feature CRUD - Asset flow: create/link asset, request signed upload URL, and toggle visibility ## Public API (current) ### Class: `GeoApiClient` Source: `libs/geo-api-client/src/GeoApiClient.ts` Constructor: - `new GeoApiClient(baseUrl, storage, storageKey?)` Key methods: - **Key storage** - `ensureKeysInStorage()` - `getStoredKeys()` - `derivePublicKey(privateKey)` - `importKeys(keys)` - `exportKeys()` - `setAccessToken(token)` - **Auth** - `getServicePublicKey()` - `createChallenge(publicKey)` - `loginWithSignature(publicKey, privateKey)` - `registerBySigningServiceKey(publicKey, privateKey)` - `createInvitation(payload, inviterPrivateKey)` - `registerWithInvitation(...)` - **Collections and features** - `listCollections()` - `createCollection(name)` - `updateCollection(collectionId, name)` - `deleteCollection(collectionId)` - `listFeatures(collectionId)` (typed with `properties.assets`) - `createPointFeature(collectionId, lon, lat, properties)` - `deleteFeature(featureId)` - **Assets (new)** - `createOrLinkAsset({...})` — create metadata or reuse existing asset by checksum/ext and link it to a feature - `getAssetSignedUploadUrl(assetId, contentType?)` — get signed `PUT` URL for binary upload - `setAssetVisibility(assetId, isPublic)` — owner toggles public/private access - `resolveRelativeLink(path)` — converts backend-relative asset links to absolute URLs for browser usage ## Asset frontend contract - Feature responses include linked assets in `feature.properties.assets`. - Asset item fields include: `id`, `kind`, `name`, `description`, `checksum`, `ext`, `isPublic`, `link`. - `link` is always backend-relative (for example `/v1/assets/{id}/download`). - Frontend should call `resolveRelativeLink(link)` and must not build direct S3 URLs. ## Recommended integration flow 1. Create one `GeoApiClient` instance per backend base URL. 2. Call `ensureKeysInStorage()` when app initializes. 3. If not yet registered: call `registerBySigningServiceKey(publicKey, privateKey)` (signs the API service key and publishes your public key). 4. Use `loginWithSignature()` to obtain and set a bearer token. 5. Create collection and map features. 6. For media upload: - compute file checksum - call `createOrLinkAsset` - call `getAssetSignedUploadUrl` - upload file to signed URL 7. Render and share assets from `properties.assets` links. 8. Use `setAssetVisibility` to toggle sharing. 9. Use `importKeys`/`exportKeys` in profile settings UX. ## Registration by signing service key When `SERVICE_PUBLIC_KEY` (or `ADMIN_PUBLIC_KEY`) is set, users can register without an invitation: 1. `GET /v1/service-key` — fetch the server public key (clients use this for registration and further signed communication). 2. Sign that key with your private key. 3. `POST /v1/auth/register-by-signature` with `{ publicKey, signature }`. Server keys are generated with `./bin/gen-server-keys.sh` and stored in `etc/`. ## Example: place and upload a 3D object (`.glb`) ```ts import { GeoApiClient } from "../libs/geo-api-client/dist/index.js"; const storage = window.localStorage; const storageLike = { getItem: (key: string) => storage.getItem(key), setItem: (key: string, value: string) => storage.setItem(key, value), removeItem: (key: string) => storage.removeItem(key), }; const client = new GeoApiClient("https://tenerife.baby", storageLike); const keys = await client.ensureKeysInStorage(); // Register (ignored if already registered); then login try { await client.registerBySigningServiceKey(keys.publicKey, keys.privateKey); } catch (_) { // Already registered or registration disabled } await client.loginWithSignature(keys.publicKey, keys.privateKey); const created = await client.createCollection("My Places"); const feature = await client.createPointFeature(created.id, -16.6291, 28.4636, { name: "Santa Cruz" }); // Assume this comes from: const fileInput = document.getElementById("modelFile") as HTMLInputElement; const file = fileInput.files?.[0]; if (!file) throw new Error("Select a .glb/.gltf file first"); const toSha256Hex = async (f: File): Promise => { const buffer = await f.arrayBuffer(); const digest = await crypto.subtle.digest("SHA-256", buffer); return Array.from(new Uint8Array(digest)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); }; const checksum = await toSha256Hex(file); const ext = file.name.toLowerCase().endsWith(".gltf") ? "gltf" : "glb"; // Create metadata (or reuse existing by checksum+ext) and link to feature const asset = await client.createOrLinkAsset({ featureId: feature.id, checksum, ext, kind: "3d", mimeType: file.type || (ext === "gltf" ? "model/gltf+json" : "model/gltf-binary"), sizeBytes: file.size, name: "Palm Tree Model", description: "3D object placed on map", isPublic: true, }); // Upload binary to object storage through signed URL const signed = await client.getAssetSignedUploadUrl(asset.asset.id, asset.asset.mimeType); await fetch(signed.url, { method: signed.method, headers: asset.asset.mimeType ? { "Content-Type": asset.asset.mimeType } : undefined, body: file, }); // Read shareable relative link from feature payload const features = await client.listFeatures(created.id); const firstAsset = features.features[0]?.properties?.assets?.[0]; if (firstAsset) { const shareUrl = client.resolveRelativeLink(firstAsset.link); console.log("Share URL:", shareUrl); } // Optional: owner can disable public access later await client.setAssetVisibility(asset.asset.id, false); ``` ## Security notes - Private keys are currently stored in browser storage via the selected storage adapter. - If your frontend has stronger security requirements, wrap the storage adapter with your own encryption/decryption layer before calling `setItem`/`getItem`. - Never send private keys to the backend. ## No-build frontend compatibility For no-bundler apps, import the built ESM file: ```html ``` The backend itself serves static UI at `/web/`, but this library can be consumed by any frontend runtime that supports `fetch`, `TextEncoder`, and ES modules. For local development you can switch the client base URL to `http://localhost:8122`.