# 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 backend upload URL, upload binary, 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 backend upload endpoint (`PUT /v1/assets/{id}/upload`)
- `uploadAssetBinary(assetId, payload, contentType?)` — upload binary through backend endpoint
- `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 `uploadAssetBinary` (or call `getAssetSignedUploadUrl` + manual `fetch`)
   - upload file to backend endpoint
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: <input id="modelFile" type="file" accept=".glb,.gltf" />
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<string> => {
  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 through backend upload endpoint
await client.uploadAssetBinary(
  asset.asset.id,
  file,
  asset.asset.mimeType || "application/octet-stream"
);

// 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
<script type="module">
  import { GeoApiClient } from "../libs/geo-api-client/dist/index.js";
  // use client...
</script>
```

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`.
