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 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`. Local default (for development): `http://localhost:8122`.
@@ -71,7 +71,7 @@ COMPOSE_BAKE=true docker compose --profile dev up --watch
Notes: 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` service uses the production `runtime` image target.
- `api-dev` profile uses the `dev` image target and Docker Compose watch. - `api-dev` profile uses the `dev` image target and Docker Compose watch.
- DB defaults can be overridden via `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`. - DB defaults can be overridden via `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`.
@@ -89,7 +89,7 @@ go run ./cmd/api
Then visit: Then visit:
- Production: `https://momswap.produktor.duckdns.org/web/` - Production: `https://tenerife.baby/web/`
- Local: `http://localhost:8122/web/` - Local: `http://localhost:8122/web/`
- Local Leaflet demo: `http://localhost:8122/web/leaflet-demo.html` - Local Leaflet demo: `http://localhost:8122/web/leaflet-demo.html`
+4 -4
View File
@@ -51,13 +51,13 @@ sequenceDiagram
```bash ```bash
# Step 1: Get challenge # 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" \ -H "Content-Type: application/json" \
-d '{"publicKey":"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI"}') -d '{"publicKey":"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI"}')
NONCE=$(echo "$CHALLENGE" | jq -r '.nonce') NONCE=$(echo "$CHALLENGE" | jq -r '.nonce')
# Step 2: Sign "login:$NONCE" with your private key, then: # 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" \ -H "Content-Type: application/json" \
-d "{\"publicKey\":\"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI\",\"nonce\":\"$NONCE\",\"signature\":\"<your_signature_base64url>\"}" -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 ```bash
# Step 1: Fetch service key # 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: # 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" \ -H "Content-Type: application/json" \
-d "{\"publicKey\":\"<your_pubkey_base64url>\",\"signature\":\"<signature_base64url>\"}" -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: 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 ## Goals
@@ -33,60 +33,60 @@ bun test # unit + integration tests (docs flow)
bun run build 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) ## Public API (current)
### Class: `GeoApiClient` ### 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: 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 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. - **Auth**
- [`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. - `getServicePublicKey()`
- [`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. - `createChallenge(publicKey)`
- [`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). - `loginWithSignature(publicKey, privateKey)`
- [`exportKeys()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L41) — Read stored keypair for export/backup. - `registerBySigningServiceKey(publicKey, privateKey)`
- [`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. - `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). - **Assets (new)**
- [`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. - `createOrLinkAsset({...})` — create metadata or reuse existing asset by checksum/ext and link it to a feature
- [`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. - `getAssetSignedUploadUrl(assetId, contentType?)` — get signed `PUT` URL for binary upload
- [`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. - `setAssetVisibility(assetId, isPublic)` — owner toggles public/private access
- [`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. - `resolveRelativeLink(path)` — converts backend-relative asset links to absolute URLs for browser usage
- [`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.
**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. - Feature responses include linked assets in `feature.properties.assets`.
- [`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. - Asset item fields include: `id`, `kind`, `name`, `description`, `checksum`, `ext`, `isPublic`, `link`.
- [`updateCollection(collectionId, name)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L140) — Rename a collection. - `link` is always backend-relative (for example `/v1/assets/{id}/download`).
- [`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. - Frontend should call `resolveRelativeLink(link)` and must not build direct S3 URLs.
**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.
## Recommended integration flow ## Recommended integration flow
@@ -94,8 +94,15 @@ Current frontend contract points:
2. Call `ensureKeysInStorage()` when app initializes. 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). 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. 4. Use `loginWithSignature()` to obtain and set a bearer token.
5. Call collection/feature methods after authentication. 5. Create collection and map features.
6. Use `importKeys`/`exportKeys` in profile settings UX. 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 ## Registration by signing service key
@@ -119,7 +126,7 @@ const storageLike = {
removeItem: (key: string) => storage.removeItem(key), 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(); const keys = await client.ensureKeysInStorage();
// Register (ignored if already registered); then login // Register (ignored if already registered); then login
@@ -131,8 +138,29 @@ try {
await client.loginWithSignature(keys.publicKey, keys.privateKey); await client.loginWithSignature(keys.publicKey, keys.privateKey);
const created = await client.createCollection("My Places"); 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 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); console.log(features);
``` ```
+1 -1
View File
@@ -29,7 +29,7 @@ export class GeoApiClient {
private accessToken: string | null = null; 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 storage - Storage adapter (localStorage-like: getItem, setItem, removeItem)
* @param storageKey - Key for persisting keypair (default from DEFAULT_KEYS_STORAGE_KEY) * @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({ createApp({
setup() { 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({ const state = reactive({
publicKey: "", publicKey: "",
privateKey: "", privateKey: "",