learning_ai_common_plat/packages/org-client/src/client.ts
saravanakumardb1 be03efa111 feat(shared-packages): add 9 @bytelyst/* client packages with 100% API coverage
Packages added:
- @bytelyst/referral-client — referral API client + share helpers
- @bytelyst/subscription-client — subscription/plan API client + cache
- @bytelyst/celebrations — milestone triggers, confetti, positive messages
- @bytelyst/gentle-notifications — ND-friendly messaging, forbidden phrases
- @bytelyst/accessibility — VoiceOver/TalkBack label generators
- @bytelyst/quick-actions — progressive disclosure, smart defaults
- @bytelyst/time-references — familiar duration references
- @bytelyst/org-client — org/workspace/membership/license API client
- @bytelyst/marketplace-client — listing/review/install API client

All packages: pure TS, ESM, globalThis.fetch, no Node.js deps.
99 Vitest tests across 9 packages, 79/79 public methods covered.

Review fixes applied:
- time-references: fix module-level mutable state leak + add clearCustomReferences()
- accessibility: fix parameter reassignment in formatDurationForA11y/numberToWords
- subscription-client: fix flaky daysRemaining test (ms boundary race)
2026-03-19 13:10:09 -07:00

225 lines
7.5 KiB
TypeScript

/**
* 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<string, string> {
const h: Record<string, string> = {
'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<OrganizationDoc[]> {
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<OrganizationDoc> {
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<OrganizationDoc> {
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<OrganizationDoc>
): Promise<OrganizationDoc> {
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<WorkspaceDoc[]> {
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<WorkspaceDoc> {
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<WorkspaceDoc>
): Promise<WorkspaceDoc> {
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<MembershipDoc[]> {
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<MembershipDoc> {
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<MembershipDoc> {
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<LicenseDoc> {
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<LicenseDoc> {
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<void> {
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,
};
}