import { StarfaceConnection, StarfaceAddressBook, UnifiedContact, emptyContact, } from "../models/types"; interface LoginResponse { loginType: string; nonce: string; secret: string | null; } interface StarfaceContactAttribute { displayKey: string; name: string; value: string; i18nDisplayName?: string; additionalValues?: Record; } interface StarfaceContactBlock { name: string; resourceKey: string; attributes: StarfaceContactAttribute[]; } interface StarfaceContact { id: string; blocks: StarfaceContactBlock[]; } interface StarfaceTag { id: string; name: string; } async function sha512(input: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(input); const hashBuffer = await crypto.subtle.digest("SHA-512", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); } export class StarfaceApiClient { private baseUrl: string; private token: string | null = null; private connection: StarfaceConnection; constructor(connection: StarfaceConnection) { this.connection = connection; const protocol = connection.useSsl ? "https" : "http"; const portPart = (connection.useSsl && connection.port === 443) || (!connection.useSsl && connection.port === 80) ? "" : `:${connection.port}`; this.baseUrl = `${protocol}://${connection.host}${portPart}/rest`; } private headers(withAuth = true): Record { const h: Record = { "Content-Type": "application/json", "X-Version": "2", }; if (withAuth && this.token) { h["authToken"] = this.token; } return h; } async login(): Promise { try { // Step 1: get nonce const nonceResp = await fetch(`${this.baseUrl}/login`, { method: "GET", headers: this.headers(false), }); if (!nonceResp.ok) throw new Error(`Login GET failed: ${nonceResp.status}`); const loginData: LoginResponse = await nonceResp.json(); // Step 2: calculate secret const { loginId, password } = this.connection; const passwordHash = await sha512(password); const combined = loginId + loginData.nonce + passwordHash; const combinedHash = await sha512(combined); const secret = `${loginId}:${combinedHash}`; // Step 3: post login const loginResp = await fetch(`${this.baseUrl}/login`, { method: "POST", headers: this.headers(false), body: JSON.stringify({ loginType: loginData.loginType, nonce: loginData.nonce, secret, }), }); if (!loginResp.ok) throw new Error(`Login POST failed: ${loginResp.status}`); const tokenData = await loginResp.json(); this.token = tokenData.token; return true; } catch (err) { console.error("Starface login failed:", err); return false; } } async logout(): Promise { if (!this.token) return; try { await fetch(`${this.baseUrl}/login`, { method: "DELETE", headers: this.headers(), }); } finally { this.token = null; } } async getCurrentUserId(): Promise { try { const resp = await fetch(`${this.baseUrl}/users/me`, { headers: this.headers(), }); if (!resp.ok) return null; const user = await resp.json(); return user.id || null; } catch { return null; } } async getTags(): Promise { try { const resp = await fetch(`${this.baseUrl}/contacts/tags`, { headers: this.headers(), }); if (!resp.ok) return []; return await resp.json(); } catch { return []; } } async getAvailableAddressBooks(): Promise { const books: StarfaceAddressBook[] = []; // Central address book books.push({ type: "central", name: "Zentrales Adressbuch", }); // Current user's private address book const userId = await this.getCurrentUserId(); if (userId) { books.push({ type: "user", userId, name: "Persönliches Adressbuch", }); } // Tags as virtual address books const tags = await this.getTags(); for (const tag of tags) { books.push({ type: "tag", tagId: tag.id, name: `Tag: ${tag.name}`, }); } return books; } async getContacts( addressBook: StarfaceAddressBook, page = 0, pageSize = 200 ): Promise { const params = new URLSearchParams({ page: page.toString(), pagesize: pageSize.toString(), }); if (addressBook.type === "user" && addressBook.userId) { params.set("userId", addressBook.userId); } if (addressBook.type === "tag" && addressBook.tagId) { params.set("tags", addressBook.tagId); } const allContacts: UnifiedContact[] = []; let currentPage = page; let hasMore = true; while (hasMore) { params.set("page", currentPage.toString()); const resp = await fetch(`${this.baseUrl}/contacts?${params}`, { headers: this.headers(), }); if (!resp.ok) break; const contacts: StarfaceContact[] = await resp.json(); if (contacts.length === 0) { hasMore = false; break; } for (const sc of contacts) { allContacts.push(this.mapFromStarface(sc)); } if (contacts.length < pageSize) { hasMore = false; } else { currentPage++; } } return allContacts; } async createContact( contact: UnifiedContact, addressBook: StarfaceAddressBook ): Promise { const starfaceContact = this.mapToStarface(contact, addressBook); const params = new URLSearchParams(); if (addressBook.type === "user" && addressBook.userId) { params.set("userId", addressBook.userId); } const url = `${this.baseUrl}/contacts${params.toString() ? "?" + params : ""}`; const resp = await fetch(url, { method: "POST", headers: this.headers(), body: JSON.stringify(starfaceContact), }); if (!resp.ok) return null; const created: StarfaceContact = await resp.json(); return this.mapFromStarface(created); } async updateContact( contactId: string, contact: UnifiedContact, addressBook: StarfaceAddressBook ): Promise { const starfaceContact = this.mapToStarface(contact, addressBook); starfaceContact.id = contactId; const params = new URLSearchParams(); if (addressBook.type === "user" && addressBook.userId) { params.set("userId", addressBook.userId); } const url = `${this.baseUrl}/contacts/${contactId}${params.toString() ? "?" + params : ""}`; const resp = await fetch(url, { method: "PUT", headers: this.headers(), body: JSON.stringify(starfaceContact), }); return resp.ok; } async deleteContact(contactId: string): Promise { const resp = await fetch(`${this.baseUrl}/contacts/${contactId}`, { method: "DELETE", headers: this.headers(), }); return resp.ok; } private mapFromStarface(sc: StarfaceContact): UnifiedContact { const contact = emptyContact(); contact.starfaceId = sc.id; const attrs: Record = {}; for (const block of sc.blocks || []) { for (const attr of block.attributes || []) { if (attr.value) { attrs[attr.displayKey] = attr.value; } } } contact.firstName = attrs["NAME"] || ""; contact.lastName = attrs["SURNAME"] || ""; contact.company = attrs["COMPANY"] || ""; contact.jobTitle = attrs["JOB_TITLE"] || ""; contact.email = attrs["EMAIL"] || ""; contact.phoneWork = attrs["OFFICE_PHONE_NUMBER"] || ""; contact.phoneMobile = attrs["MOBILE_PHONE_NUMBER"] || ""; contact.phoneHome = attrs["PRIVATE_PHONE_NUMBER"] || ""; contact.fax = attrs["FAX_NUMBER"] || ""; contact.street = attrs["STREET"] || ""; contact.city = attrs["CITY"] || ""; contact.postalCode = attrs["POSTAL_CODE"] || ""; contact.state = attrs["STATE"] || ""; contact.country = attrs["COUNTRY"] || ""; contact.website = attrs["URL"] || ""; contact.notes = attrs["NOTE"] || ""; contact.salutation = attrs["SALUTATION"] || ""; contact.title = attrs["TITLE"] || ""; contact.birthday = attrs["BIRTHDAY"] || ""; return contact; } private mapToStarface( contact: UnifiedContact, addressBook: StarfaceAddressBook ): StarfaceContact { const attributes: StarfaceContactAttribute[] = []; const addAttr = (displayKey: string, name: string, value: string) => { if (value) { attributes.push({ displayKey, name: name.toLowerCase(), value }); } }; addAttr("NAME", "firstName", contact.firstName); addAttr("SURNAME", "lastName", contact.lastName); addAttr("COMPANY", "company", contact.company); addAttr("JOB_TITLE", "jobTitle", contact.jobTitle); addAttr("EMAIL", "email", contact.email); addAttr("OFFICE_PHONE_NUMBER", "businessPhone", contact.phoneWork); addAttr("MOBILE_PHONE_NUMBER", "mobilePhone", contact.phoneMobile); addAttr("PRIVATE_PHONE_NUMBER", "homePhone", contact.phoneHome); addAttr("FAX_NUMBER", "fax", contact.fax); addAttr("STREET", "street", contact.street); addAttr("CITY", "city", contact.city); addAttr("POSTAL_CODE", "postalCode", contact.postalCode); addAttr("STATE", "state", contact.state); addAttr("COUNTRY", "country", contact.country); addAttr("URL", "website", contact.website); addAttr("NOTE", "notes", contact.notes); addAttr("SALUTATION", "salutation", contact.salutation); addAttr("TITLE", "title", contact.title); addAttr("BIRTHDAY", "birthday", contact.birthday); const sc: StarfaceContact = { id: contact.starfaceId || "", blocks: [ { name: "contact", resourceKey: "contact", attributes, }, ], }; return sc; } }