Update docs and defaults for tenerife.baby domain.
CI / test (push) Successful in 3s

This replaces old momswap.produktor.duckdns.org references with tenerife.baby and refreshes the TypeScript integration guide to reflect the current asset upload, sharing, and relative-link flow.

Made-with: Cursor
This commit is contained in:
2026-03-02 21:31:21 +00:00
parent e00280b653
commit efe5907adc
5 changed files with 81 additions and 53 deletions
+3 -3
View File
@@ -26,7 +26,7 @@ Run tests via Docker (avoids local permission issues, e.g. `var/`):
docker compose --profile test run --rm test
```
Primary deployed base URL: `https://momswap.produktor.duckdns.org/`.
Primary deployed base URL: `https://tenerife.baby/`.
Local default (for development): `http://localhost:8122`.
@@ -71,7 +71,7 @@ COMPOSE_BAKE=true docker compose --profile dev up --watch
Notes:
- `api` service listens on `8122` inside the container, mapped to host `8122` (reverse proxy at `https://momswap.produktor.duckdns.org`).
- `api` service listens on `8122` inside the container, mapped to host `8122` (reverse proxy at `https://tenerife.baby`).
- `api` service uses the production `runtime` image target.
- `api-dev` profile uses the `dev` image target and Docker Compose watch.
- DB defaults can be overridden via `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`.
@@ -89,7 +89,7 @@ go run ./cmd/api
Then visit:
- Production: `https://momswap.produktor.duckdns.org/web/`
- Production: `https://tenerife.baby/web/`
- Local: `http://localhost:8122/web/`
- Local Leaflet demo: `http://localhost:8122/web/leaflet-demo.html`
+4 -4
View File
@@ -51,13 +51,13 @@ sequenceDiagram
```bash
# Step 1: Get challenge
CHALLENGE=$(curl -s -X POST https://momswap.produktor.duckdns.org/v1/auth/challenge \
CHALLENGE=$(curl -s -X POST https://tenerife.baby/v1/auth/challenge \
-H "Content-Type: application/json" \
-d '{"publicKey":"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI"}')
NONCE=$(echo "$CHALLENGE" | jq -r '.nonce')
# Step 2: Sign "login:$NONCE" with your private key, then:
curl -s -X POST https://momswap.produktor.duckdns.org/v1/auth/login \
curl -s -X POST https://tenerife.baby/v1/auth/login \
-H "Content-Type: application/json" \
-d "{\"publicKey\":\"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI\",\"nonce\":\"$NONCE\",\"signature\":\"<your_signature_base64url>\"}"
```
@@ -91,10 +91,10 @@ Registers a new user without an invitation. User proves key ownership by signing
```bash
# Step 1: Fetch service key
SERVICE_KEY=$(curl -s https://momswap.produktor.duckdns.org/v1/service-key | jq -r '.publicKey')
SERVICE_KEY=$(curl -s https://tenerife.baby/v1/service-key | jq -r '.publicKey')
# Step 2: Sign $SERVICE_KEY with your private key, then:
curl -s -X POST https://momswap.produktor.duckdns.org/v1/auth/register-by-signature \
curl -s -X POST https://tenerife.baby/v1/auth/register-by-signature \
-H "Content-Type: application/json" \
-d "{\"publicKey\":\"<your_pubkey_base64url>\",\"signature\":\"<signature_base64url>\"}"
```
+72 -44
View File
@@ -8,9 +8,9 @@ This document explains how frontend developers should integrate with the backend
Primary backend URL for integration:
- `https://momswap.produktor.duckdns.org/`
- `https://tenerife.baby/`
Deployment: API is proxied via reverse proxy from `https://momswap.produktor.duckdns.org` to backend at `172.17.0.1:8122`. Docker Compose maps port 8122 for the reverse proxy.
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
@@ -33,60 +33,60 @@ bun test # unit + integration tests (docs flow)
bun run build
```
Integration tests in `test/integration.test.ts` cover the recommended flow: register, login, create collection, create point feature, list features.
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: [GeoApiClient.ts](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts)
Source: `libs/geo-api-client/src/GeoApiClient.ts`
Constructor:
- [`new GeoApiClient(baseUrl, storage, storageKey?)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L14)
- `new GeoApiClient(baseUrl, storage, storageKey?)`
Key methods:
**Key storage**
- **Key storage**
- `ensureKeysInStorage()`
- `getStoredKeys()`
- `derivePublicKey(privateKey)`
- `importKeys(keys)`
- `exportKeys()`
- `setAccessToken(token)`
- [`ensureKeysInStorage()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L20) — Ensure a keypair exists; if none found, generate and save. Use on app init.
- [`getStoredKeys()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L29) — Get stored keypair without generating. Returns null if none.
- [`derivePublicKey(privateKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L33) — Derive public key from private key (Ed25519). Use when importing pk from backup/QR.
- [`importKeys(keys)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L37) — Overwrite stored keypair (e.g. after import or restore from QR).
- [`exportKeys()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L41) — Read stored keypair for export/backup.
- [`setAccessToken(token)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L45) — Set bearer token for authenticated requests. Call after login.
- **Auth**
- `getServicePublicKey()`
- `createChallenge(publicKey)`
- `loginWithSignature(publicKey, privateKey)`
- `registerBySigningServiceKey(publicKey, privateKey)`
- `createInvitation(payload, inviterPrivateKey)`
- `registerWithInvitation(...)`
**Auth**
- **Collections and features**
- `listCollections()`
- `createCollection(name)`
- `updateCollection(collectionId, name)`
- `deleteCollection(collectionId)`
- `listFeatures(collectionId)` (typed with `properties.assets`)
- `createPointFeature(collectionId, lon, lat, properties)`
- `deleteFeature(featureId)`
- [`getServicePublicKey()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L69) — Fetch API service public key (for register-by-signature).
- [`createChallenge(publicKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L73) — Request login challenge; returns nonce and messageToSign.
- [`loginWithSignature(publicKey, privateKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L86) — Login via challenge-response. Returns bearer token; stores it internally.
- [`registerBySigningServiceKey(publicKey, privateKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L76) — Register without invitation by signing the API service key. 409 if already registered.
- [`createInvitation(payload, inviterPrivateKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L101) — Create invitation for new users. Inviter signs the payload.
- [`registerWithInvitation(...)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L114) — Register using an invitation. Proves key ownership and redeems invite.
- **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
**Collections**
## Asset frontend contract
- [`listCollections()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L133) — List collections for the authenticated user.
- [`createCollection(name)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L137) — Create a new collection. Returns id and name.
- [`updateCollection(collectionId, name)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L140) — Rename a collection.
- [`deleteCollection(collectionId)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L148) — Delete a collection and its features.
**Features**
- [`listFeatures(collectionId)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L153) — List GeoJSON features in a collection.
- [`createPointFeature(collectionId, lon, lat, properties)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L156) — Add a Point. lon ∈ [-180,180], lat ∈ [-90,90]. Returns feature id.
- [`deleteFeature(featureId)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L172) — Delete a feature.
## Asset API integration note
Asset endpoints are currently available at backend API level (`/v1/assets...`) and can be called from frontend apps directly with authenticated `fetch` requests.
Current frontend contract points:
- Feature list responses include linked media under `feature.properties.assets`.
- Each asset includes a backend-relative download path (`link`) like `/v1/assets/{id}/download`.
- Frontend should use this relative path and avoid constructing direct S3 URLs.
- 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
@@ -94,8 +94,15 @@ Current frontend contract points:
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. Call collection/feature methods after authentication.
6. Use `importKeys`/`exportKeys` in profile settings UX.
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
@@ -119,7 +126,7 @@ const storageLike = {
removeItem: (key: string) => storage.removeItem(key),
};
const client = new GeoApiClient("https://momswap.produktor.duckdns.org", storageLike);
const client = new GeoApiClient("https://tenerife.baby", storageLike);
const keys = await client.ensureKeysInStorage();
// Register (ignored if already registered); then login
@@ -131,8 +138,29 @@ try {
await client.loginWithSignature(keys.publicKey, keys.privateKey);
const created = await client.createCollection("My Places");
await client.createPointFeature(created.id, -16.6291, 28.4636, { name: "Santa Cruz" });
const feature = await client.createPointFeature(created.id, -16.6291, 28.4636, { name: "Santa Cruz" });
// 3D/image asset flow
const asset = await client.createOrLinkAsset({
featureId: feature.id,
checksum: "sha256hex...",
ext: "glb",
kind: "3d",
mimeType: "model/gltf-binary",
sizeBytes: 1024,
name: "Palm Tree",
description: "Low-poly model",
isPublic: true,
});
const signed = await client.getAssetSignedUploadUrl(asset.asset.id, "model/gltf-binary");
// fetch(signed.url, { method: signed.method, body: file })
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);
}
console.log(features);
```
+1 -1
View File
@@ -29,7 +29,7 @@ export class GeoApiClient {
private accessToken: string | null = null;
/**
* @param baseUrl - API base URL (e.g. https://momswap.produktor.duckdns.org)
* @param baseUrl - API base URL (e.g. https://tenerife.baby)
* @param storage - Storage adapter (localStorage-like: getItem, setItem, removeItem)
* @param storageKey - Key for persisting keypair (default from DEFAULT_KEYS_STORAGE_KEY)
*/
+1 -1
View File
@@ -6,7 +6,7 @@ const { createApp, ref, reactive, onMounted, watch } = Vue;
createApp({
setup() {
const apiBase = ref(localStorage.getItem("geo_api_base") || "https://momswap.produktor.duckdns.org");
const apiBase = ref(localStorage.getItem("geo_api_base") || "https://tenerife.baby");
const state = reactive({
publicKey: "",
privateKey: "",