/** * Browser/React Native-safe org, workspace, membership, and license client * for platform-service. * * All org routes require admin-only access (super_admin or admin JWT role). * No Node.js dependencies — uses globalThis.fetch. */ import type { LicenseDoc, MembershipDoc, OrgClient, OrgClientConfig, OrganizationDoc, WorkspaceDoc, } from './types.js'; function generateRequestId(): string { return typeof globalThis.crypto?.randomUUID === 'function' ? globalThis.crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; } export function createOrgClient(config: OrgClientConfig): OrgClient { const { baseUrl, productId, getAccessToken } = config; function headers(): Record { const h: Record = { 'Content-Type': 'application/json', 'x-product-id': productId, 'x-request-id': generateRequestId(), }; const token = getAccessToken(); if (token) h['Authorization'] = `Bearer ${token}`; return h; } // ── Organizations ───────────────────────────────── async function listOrgs(query?: { status?: string; limit?: number }): Promise { const params = new URLSearchParams(); if (query?.status) params.set('status', query.status); if (query?.limit) params.set('limit', String(query.limit)); const qs = params.toString(); const url = qs ? `${baseUrl}/orgs?${qs}` : `${baseUrl}/orgs`; const res = await globalThis.fetch(url, { headers: headers() }); if (!res.ok) throw new Error(`listOrgs failed: ${res.status}`); return (await res.json()) as OrganizationDoc[]; } async function createOrg(input: { name: string; slug: string; ownerUserId?: string; }): Promise { const res = await globalThis.fetch(`${baseUrl}/orgs`, { method: 'POST', headers: headers(), body: JSON.stringify({ ...input, productId }), }); if (!res.ok) throw new Error(`createOrg failed: ${res.status}`); return (await res.json()) as OrganizationDoc; } async function getOrg(id: string): Promise { const res = await globalThis.fetch(`${baseUrl}/orgs/${encodeURIComponent(id)}`, { headers: headers(), }); if (!res.ok) throw new Error(`getOrg failed: ${res.status}`); return (await res.json()) as OrganizationDoc; } async function updateOrg( id: string, updates: Partial ): Promise { const res = await globalThis.fetch(`${baseUrl}/orgs/${encodeURIComponent(id)}`, { method: 'PATCH', headers: headers(), body: JSON.stringify(updates), }); if (!res.ok) throw new Error(`updateOrg failed: ${res.status}`); return (await res.json()) as OrganizationDoc; } // ── Workspaces ──────────────────────────────────── async function listWorkspaces(orgId: string): Promise { const res = await globalThis.fetch(`${baseUrl}/orgs/${encodeURIComponent(orgId)}/workspaces`, { headers: headers(), }); if (!res.ok) throw new Error(`listWorkspaces failed: ${res.status}`); return (await res.json()) as WorkspaceDoc[]; } async function createWorkspace( orgId: string, input: { name: string; slug: string; description?: string } ): Promise { const res = await globalThis.fetch(`${baseUrl}/orgs/${encodeURIComponent(orgId)}/workspaces`, { method: 'POST', headers: headers(), body: JSON.stringify(input), }); if (!res.ok) throw new Error(`createWorkspace failed: ${res.status}`); return (await res.json()) as WorkspaceDoc; } async function updateWorkspace( orgId: string, workspaceId: string, updates: Partial ): Promise { const res = await globalThis.fetch( `${baseUrl}/orgs/${encodeURIComponent(orgId)}/workspaces/${encodeURIComponent(workspaceId)}`, { method: 'PATCH', headers: headers(), body: JSON.stringify(updates), } ); if (!res.ok) throw new Error(`updateWorkspace failed: ${res.status}`); return (await res.json()) as WorkspaceDoc; } // ── Memberships ─────────────────────────────────── async function listMemberships( orgId: string, query?: { scope?: string; limit?: number } ): Promise { const params = new URLSearchParams(); if (query?.scope) params.set('scope', query.scope); if (query?.limit) params.set('limit', String(query.limit)); const qs = params.toString(); const url = qs ? `${baseUrl}/orgs/${encodeURIComponent(orgId)}/memberships?${qs}` : `${baseUrl}/orgs/${encodeURIComponent(orgId)}/memberships`; const res = await globalThis.fetch(url, { headers: headers() }); if (!res.ok) throw new Error(`listMemberships failed: ${res.status}`); return (await res.json()) as MembershipDoc[]; } async function addMember( orgId: string, input: { userId: string; role?: string; scope?: string; workspaceId?: string } ): Promise { const res = await globalThis.fetch(`${baseUrl}/orgs/${encodeURIComponent(orgId)}/memberships`, { method: 'POST', headers: headers(), body: JSON.stringify(input), }); if (!res.ok) throw new Error(`addMember failed: ${res.status}`); return (await res.json()) as MembershipDoc; } async function updateMember( orgId: string, membershipId: string, updates: { role?: string; status?: string } ): Promise { const res = await globalThis.fetch( `${baseUrl}/orgs/${encodeURIComponent(orgId)}/memberships/${encodeURIComponent(membershipId)}`, { method: 'PATCH', headers: headers(), body: JSON.stringify(updates), } ); if (!res.ok) throw new Error(`updateMember failed: ${res.status}`); return (await res.json()) as MembershipDoc; } // ── Licenses ────────────────────────────────────── async function generateLicense(input: { userId: string; plan: string; maxDevices?: number; }): Promise { const res = await globalThis.fetch(`${baseUrl}/licenses`, { method: 'POST', headers: headers(), body: JSON.stringify({ ...input, productId }), }); if (!res.ok) throw new Error(`generateLicense failed: ${res.status}`); return (await res.json()) as LicenseDoc; } async function activateLicense(input: { key: string; deviceId: string }): Promise { const res = await globalThis.fetch(`${baseUrl}/licenses/activate`, { method: 'POST', headers: headers(), body: JSON.stringify(input), }); if (!res.ok) throw new Error(`activateLicense failed: ${res.status}`); return (await res.json()) as LicenseDoc; } async function deactivateLicense(input: { key: string; deviceId: string }): Promise { const res = await globalThis.fetch(`${baseUrl}/licenses/deactivate`, { method: 'POST', headers: headers(), body: JSON.stringify(input), }); if (!res.ok) throw new Error(`deactivateLicense failed: ${res.status}`); } return { listOrgs, createOrg, getOrg, updateOrg, listWorkspaces, createWorkspace, updateWorkspace, listMemberships, addMember, updateMember, generateLicense, activateLicense, deactivateLicense, }; }