/** * @module api/ibgib-api-bridge * * Base REST API bridge class for interacting with serve-gib endpoints from browser clients. */ import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs"; import { FlatIbGibGraph } from "@ibgib/core-gib/dist/common/other/graph-types.mjs"; const logalot = false; export class IbGibApiBridge { protected lc: string = `[${IbGibApiBridge.name}]`; protected async parseResponseJson(response: Response, url: string): Promise { const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { const text = await response.text(); if (text.trim().toLowerCase().startsWith(' { const lc = `${this.lc}[${this.postGenesisKeystone.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: eecd082fc4a4381c8857928e8b090826)`); } const url = '/api/keystone/genesis'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ keystoneIbGib, siteUsername: options?.siteUsername, siteUsernameExceptionKey: options?.siteUsernameExceptionKey, }), }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Unknown error'; console.error(`${lc} server rejected genesis keystone: ${errorMsg}`); return { success: false, message: errorMsg, addr: data.addr, statusCode: response.status }; } if (logalot) { console.log(`${lc} genesis keystone posted successfully. pending: ${data.pending}`); } return { success: true, pending: !!data.pending, statusCode: response.status, addr: data.addr, siteUsername: data.siteUsername, message: data.message }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * PUTs an evolved domain keystone and any related keystones to the server. */ public async putEvolveKeystone({ domainAddr, keystoneIbGib, relatedKeystoneIbGibs = [], revokeTokens }: { domainAddr: string; keystoneIbGib: IbGib_V1; relatedKeystoneIbGibs?: IbGib_V1[]; revokeTokens?: Record; }): Promise<{ success: boolean; message?: string }> { const lc = `${this.lc}[${this.putEvolveKeystone.name}]`; try { if (logalot) { console.log(`${lc} posting evolved keystone...`); } const url = `/api/keystone/evolve/${encodeURIComponent(domainAddr)}`; const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ keystoneIbGib, relatedKeystoneIbGibs, revokeTokens }), }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Unknown error'; console.error(`${lc} server rejected keystone evolution: ${errorMsg}`); return { success: false, message: errorMsg }; } if (logalot) { console.log(`${lc} keystone evolution accepted by server.`); } return { success: true }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} fetch error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * Looks up a primary identity keystone on the server by username or email. */ public async lookupPrimaryKeystone({ identifier }: { identifier: string }): Promise<{ success: boolean; message?: string; tjpAddr?: string; siteUsername?: string; primaryKeystoneIbGib?: IbGib_V1; }> { const lc = `${this.lc}[${this.lookupPrimaryKeystone.name}]`; try { const url = '/api/keystone/lookup'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ identifier }), }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Identity not found'; console.error(`${lc} server lookup failed: ${errorMsg}`); return { success: false, message: errorMsg }; } return { success: true, tjpAddr: data.tjpAddr, siteUsername: data.siteUsername, primaryKeystoneIbGib: data.primaryKeystoneIbGib }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} lookup fetch error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * Checks if an email address is available on the server before local genesis. */ public async checkEmailAvailability({ email }: { email: string }): Promise<{ available: boolean; message?: string }> { const lc = `${this.lc}[${this.checkEmailAvailability.name}]`; try { const url = `/api/keystone/check-email?email=${encodeURIComponent(email)}`; const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Server check failed'; console.error(`${lc} email check failed: ${errorMsg}`); return { available: false, message: errorMsg }; } return { available: data.available ?? true, message: data.message }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} check email error: ${errorMsg}`); return { available: true }; // network error fallback } } /** * Checks if a site username is available on the server before local genesis. */ public async checkSiteUsernameAvailability({ siteUsername }: { siteUsername: string }): Promise<{ available: boolean; requiresExceptionKey?: boolean; reason?: string; message?: string; retryAfterMs?: number; }> { const lc = `${this.lc}[${this.checkSiteUsernameAvailability.name}]`; try { const url = `/api/keystone/check-site-username/${encodeURIComponent(siteUsername.trim())}`; const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Server check failed'; console.error(`${lc} site username check failed: ${errorMsg}`); return { available: false, message: errorMsg }; } return { available: data.available ?? true, requiresExceptionKey: !!data.requiresExceptionKey, reason: data.reason, message: data.message, retryAfterMs: data.retryAfterMs, }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} check site username error: ${errorMsg}`); return { available: false, message: errorMsg }; } } /** * Submits an application for a developer program short handle / brand exception key. */ public async requestSiteUsernameException({ siteUsername, email, applicantName, projectTitle, reason, profileUrl }: { siteUsername: string; email: string; applicantName?: string; projectTitle?: string; reason: string; profileUrl?: string; }): Promise<{ success: boolean; requestId?: string; message?: string }> { const lc = `${this.lc}[${this.requestSiteUsernameException.name}]`; try { const url = '/api/keystone/request-site-username-exception'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ siteUsername, email, applicantName, projectTitle, reason, profileUrl, }), }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Server rejected exception request'; console.error(`${lc} exception request failed: ${errorMsg}`); return { success: false, message: errorMsg }; } return { success: true, requestId: data.requestId, message: data.message, }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} request exception error: ${errorMsg}`); return { success: false, message: errorMsg }; } } public async getKeystoneGraph(domainAddr: string): Promise<{ success: boolean; message?: string; graph?: Record }> { const lc = `${this.lc}[${this.getKeystoneGraph.name}]`; try { if (logalot) { console.log(`${lc} fetching keystone graph...`); } const url = `/api/keystone/${encodeURIComponent(domainAddr)}?getLatest=true&getGraph=true`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', } }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Unknown error'; console.error(`${lc} server rejected keystone graph get: ${errorMsg}`); return { success: false, message: errorMsg }; } return { success: true, graph: data.domainGraph }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} fetch error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * GETs a single ibGib frame from the server's domain space. */ public async getIbGib(domainAddr: string, ibGibAddr: string, getLatest = false): Promise<{ success: boolean; message?: string; ibGib?: IbGib_V1 }> { const lc = `${this.lc}[${this.getIbGib.name}]`; try { if (logalot) { console.log(`${lc} fetching single ibGib frame...`); } const url = `/api/ibgib/${encodeURIComponent(domainAddr)}/${encodeURIComponent(ibGibAddr)}?getLatest=${getLatest}&getGraph=false`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', } }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Unknown error'; console.error(`${lc} server rejected ibGib get: ${errorMsg}`); return { success: false, message: errorMsg }; } return { success: true, ibGib: data }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} fetch error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * GETs the full dependency graph of an ibGib from the server's domain space. */ public async getIbGibGraph(domainAddr: string, ibGibAddr: string, getLatest = false): Promise<{ success: boolean; message?: string; graph?: Record; addr?: string }> { const lc = `${this.lc}[${this.getIbGibGraph.name}]`; try { if (logalot) { console.log(`${lc} fetching ibGib graph...`); } const url = `/api/ibgib/${encodeURIComponent(domainAddr)}/${encodeURIComponent(ibGibAddr)}?getLatest=${getLatest}&getGraph=true`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', } }); const data = await response.json(); if (!response.ok) { const errorMsg = data.message || data.error || 'Unknown error'; console.error(`${lc} server rejected ibGib graph get: ${errorMsg}`); return { success: false, message: errorMsg }; } return { success: true, graph: data.graph, addr: data.addr }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} fetch error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * Posts a request to generate a 6-digit verification code, save it to pending staging, and dispatch an email. */ public async postVerificationCode({ email, primaryTjpAddr }: { email: string; primaryTjpAddr?: string; }): Promise<{ success: boolean; message?: string }> { const lc = `${this.lc}[${this.postVerificationCode.name}]`; try { const url = '/api/email/verification-code'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, primaryTjpAddr }), }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Verification code request failed'; console.error(`${lc} server verification code post failed: ${errorMsg}`); return { success: false, message: errorMsg }; } return { success: true, message: data.message }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} post verification code error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * Alias for postVerificationCode (deprecated name). * * @deprecated Use postVerificationCode instead. */ public async postMagicLink(opts: { email: string; primaryTjpAddr: string; }): Promise<{ success: boolean; message?: string }> { return this.postVerificationCode(opts); } /** * Posts a 6-digit verification code to activate an identity and verify email. */ public async postVerifyCode({ email, primaryTjpAddr, code }: { email: string; primaryTjpAddr: string; code: string; }): Promise<{ success: boolean; message?: string }> { const lc = `${this.lc}[${this.postVerifyCode.name}]`; try { const url = '/api/email/verify-code'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, primaryTjpAddr, code }), }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Code verification failed'; console.error(`${lc} server verify-code failed: ${errorMsg}`); return { success: false, message: errorMsg }; } if (logalot) { console.log(`${lc} verification code accepted.`); } return { success: true, message: data.message }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} fetch error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * Posts a custodian genesis request (`POST /api/identity/custodian/genesis`) * to obtain the primary identity keystone (or primaryTjpAddr) and a fresh, single-turn * `CustodianRequirements` payload for delegate registration. */ public async postCustodianGenesis({ email, code, username, siteUsername, siteUsernameExceptionKey, }: { email: string; code?: string; username?: string; siteUsername?: string; siteUsernameExceptionKey?: string; }): Promise<{ success: boolean; isNewIdentity?: boolean; primaryTjpAddr?: string; primaryKeystone?: IbGib_V1; primaryGraph?: FlatIbGibGraph; custodianReqs?: any; message?: string; }> { const lc = `${this.lc}[${this.postCustodianGenesis.name}]`; try { const url = '/api/identity/custodian/genesis'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, code, username, siteUsername, siteUsernameExceptionKey }), }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Custodian genesis failed'; console.error(`${lc} custodian genesis failed: ${errorMsg}`); return { success: false, message: errorMsg }; } return { success: true, isNewIdentity: !!data.isNewIdentity, primaryTjpAddr: data.primaryTjpAddr, primaryKeystone: data.primaryKeystone, primaryGraph: data.primaryGraph, custodianReqs: data.custodianReqs, message: data.message }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} post custodian genesis error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * Posts a client delegate keystone containing single-turn `custodianReqs` * to register it with the server custodian node (`POST /api/identity/custodian/register-delegate`). */ public async postCustodianRegisterDelegate({ delegateKeystone }: { delegateKeystone: IbGib_V1; }): Promise<{ success: boolean; primaryTjpAddr?: string; evolvedPrimaryKeystone?: IbGib_V1; primaryGraph?: FlatIbGibGraph; message?: string; }> { const lc = `${this.lc}[${this.postCustodianRegisterDelegate.name}]`; try { const url = '/api/identity/custodian/register-delegate'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ delegateKeystone }), }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Custodian delegate registration failed'; console.error(`${lc} custodian delegate registration failed: ${errorMsg}`); return { success: false, message: errorMsg }; } return { success: true, primaryTjpAddr: data.primaryTjpAddr, evolvedPrimaryKeystone: data.evolvedPrimaryKeystone, primaryGraph: data.primaryGraph, message: data.message }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} post custodian register delegate error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * Retrieves safe public SSO client configuration (`GET /api/identity/sso/config`). */ public async getSsoConfig(): Promise<{ success: boolean; providers?: Record; message?: string; }> { const lc = `${this.lc}[${this.getSsoConfig.name}]`; try { const url = '/api/identity/sso/config'; const response = await fetch(url, { method: 'GET' }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'Failed to fetch SSO config'; return { success: false, message: errorMsg }; } return { success: true, providers: data.providers }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} fetch SSO config error: ${errorMsg}`); return { success: false, message: errorMsg }; } } /** * Posts an OAuth authorization code (`POST /api/identity/sso/login`) * to complete SSO token exchange and obtain custodian requirements. */ public async postSsoLogin({ code, providerId, redirectUri, username, siteUsername, siteUsernameExceptionKey, }: { code: string; providerId: string; redirectUri: string; username?: string; siteUsername?: string; siteUsernameExceptionKey?: string; }): Promise<{ success: boolean; isNewIdentity?: boolean; primaryTjpAddr?: string; primaryKeystone?: IbGib_V1; primaryGraph?: FlatIbGibGraph; custodianReqs?: any; userInfo?: any; message?: string; }> { const lc = `${this.lc}[${this.postSsoLogin.name}]`; try { const url = '/api/identity/sso/login'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ code, providerId, redirectUri, username, siteUsername, siteUsernameExceptionKey }), }); const data = await this.parseResponseJson(response, url); if (!response.ok) { const errorMsg = data.message || data.error || 'SSO login code exchange failed'; console.error(`${lc} SSO login code exchange failed: ${errorMsg}`); return { success: false, message: errorMsg }; } return { success: true, isNewIdentity: !!data.isNewIdentity, primaryTjpAddr: data.primaryTjpAddr, primaryKeystone: data.primaryKeystone, primaryGraph: data.primaryGraph, custodianReqs: data.custodianReqs, userInfo: data.userInfo, message: data.message }; } catch (error) { const errorMsg = extractErrorMsg(error); console.error(`${lc} post SSO login error: ${errorMsg}`); return { success: false, message: errorMsg }; } } } let _apiBridge: IbGibApiBridge | undefined; /** * Returns the global API bridge instance, creating a default IbGibApiBridge if not set. */ export function getApiBridge(): IbGibApiBridge { if (!_apiBridge) { _apiBridge = new IbGibApiBridge(); } return _apiBridge; } /** * Registers a custom API bridge instance (e.g. SpaceGibApiBridge or app-specific subclass). */ export function setApiBridge(bridge: IbGibApiBridge): void { _apiBridge = bridge; }