Files
temetro/frontend/lib/signing.ts
T
Khalid Abdi 99aa534e88 feat: multi-clinic Temetro Network with per-clinic identity (v0.8.0)
The relay is now multi-clinic. Each clinic authenticates to the /hub
namespace by signing a challenge with its own Ed25519 clinic signing key
(a per-clinic identity, not a shared RELAY_TOKEN), and the relay routes
every device response back to only the clinic that originated the request
(keyed by requestId) — so clinics never see each other's traffic.

Backend:
- clinic_signing_keys.network_enabled + GET/PUT /api/signing/network
  (owner/admin) to join/leave the network.
- relay-client keeps one authenticated hub connection per network-enabled
  org (connectOrg/disconnectOrg, hubs map keyed by orgId); emitToWallet/
  sendToWallet take orgId; offline flush is org-scoped.
- Wallet import/push return 409 until a clinic joins.
- RELAY_TOKEN is now optional/legacy (open relay needs no shared secret).

Frontend:
- "Join Temetro Network" toggle in Settings → Signing, localized in all
  five languages (en, fr, de, so, ar).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:30:19 +03:00

59 lines
2.0 KiB
TypeScript

// Client for the clinic signing key (Settings → Signing) and the "import from a
// patient app" wallet-share flow. Both call the backend over the shared fetch
// wrapper (session cookie sent automatically).
import { apiFetch } from "@/lib/api-client";
export type SigningKey = {
algorithm: string;
publicKey: string;
fingerprint: string;
createdAt: string; // ISO
rotatedAt: string | null;
};
export type WalletShareMode = "permanent" | "temporary";
export type SharedRecord = {
id: string;
walletNumber: string;
status: "pending" | "approved" | "denied" | "expired";
shareMode: WalletShareMode;
shareExpiresAt: string | null;
// The draft is only returned by the request-share poll, not the list.
};
// The clinic's Ed25519 signing key. The backend creates one lazily on first
// read, so this always resolves to a real key + fingerprint.
export async function getSigningKey(): Promise<SigningKey> {
return apiFetch<SigningKey>("/api/signing/key");
}
// Rotate the signing key (owner/admin only). Returns the new key.
export async function rotateSigningKey(): Promise<SigningKey> {
return apiFetch<SigningKey>("/api/signing/key/rotate", { method: "POST" });
}
// Recent records shared from patient wallets — feeds the panel's list.
export async function listSignedRecords(): Promise<SharedRecord[]> {
return apiFetch<SharedRecord[]>("/api/signing/records");
}
// Whether this clinic has joined the Temetro Network relay (patient-wallet
// sharing rides it). Readable by any clinician.
export async function getNetworkEnabled(): Promise<boolean> {
const { enabled } = await apiFetch<{ enabled: boolean }>(
"/api/signing/network",
);
return enabled;
}
// Join or leave the Temetro Network (owner/admin only). Returns the new state.
export async function setNetworkEnabled(enabled: boolean): Promise<boolean> {
const res = await apiFetch<{ enabled: boolean }>("/api/signing/network", {
method: "PUT",
body: JSON.stringify({ enabled }),
});
return res.enabled;
}