learning_ai_common_plat/packages/marketplace-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

220 lines
7.6 KiB
TypeScript

/**
* Browser/React Native-safe marketplace client for platform-service.
*
* Wraps platform-service /marketplace/* endpoints.
* No Node.js dependencies — uses globalThis.fetch.
*/
import type {
CreateListingInput,
MarketplaceClient,
MarketplaceClientConfig,
MarketplaceInstallDoc,
MarketplaceListingDoc,
MarketplaceReviewDoc,
} 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 createMarketplaceClient(config: MarketplaceClientConfig): MarketplaceClient {
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;
}
// ── Listings ──────────────────────────────────────
async function listListings(query?: {
templateType?: string;
category?: string;
tags?: string;
pricingModel?: string;
sortBy?: string;
q?: string;
limit?: number;
offset?: number;
}): Promise<{ listings: MarketplaceListingDoc[]; total: number }> {
const params = new URLSearchParams();
if (query?.templateType) params.set('templateType', query.templateType);
if (query?.category) params.set('category', query.category);
if (query?.tags) params.set('tags', query.tags);
if (query?.pricingModel) params.set('pricingModel', query.pricingModel);
if (query?.sortBy) params.set('sortBy', query.sortBy);
if (query?.q) params.set('q', query.q);
if (query?.limit) params.set('limit', String(query.limit));
if (query?.offset) params.set('offset', String(query.offset));
const qs = params.toString();
const url = qs ? `${baseUrl}/marketplace/listings?${qs}` : `${baseUrl}/marketplace/listings`;
const res = await globalThis.fetch(url, { headers: headers() });
if (!res.ok) throw new Error(`listListings failed: ${res.status}`);
return (await res.json()) as { listings: MarketplaceListingDoc[]; total: number };
}
async function getListing(id: string): Promise<MarketplaceListingDoc> {
const res = await globalThis.fetch(
`${baseUrl}/marketplace/listings/${encodeURIComponent(id)}`,
{ headers: headers() }
);
if (!res.ok) throw new Error(`getListing failed: ${res.status}`);
return (await res.json()) as MarketplaceListingDoc;
}
async function createListing(input: CreateListingInput): Promise<MarketplaceListingDoc> {
const res = await globalThis.fetch(`${baseUrl}/marketplace/listings`, {
method: 'POST',
headers: headers(),
body: JSON.stringify(input),
});
if (!res.ok) throw new Error(`createListing failed: ${res.status}`);
return (await res.json()) as MarketplaceListingDoc;
}
async function updateListing(
id: string,
updates: Partial<MarketplaceListingDoc>
): Promise<MarketplaceListingDoc> {
const res = await globalThis.fetch(
`${baseUrl}/marketplace/listings/${encodeURIComponent(id)}`,
{
method: 'PATCH',
headers: headers(),
body: JSON.stringify(updates),
}
);
if (!res.ok) throw new Error(`updateListing failed: ${res.status}`);
return (await res.json()) as MarketplaceListingDoc;
}
async function submitForCertification(
id: string,
notes?: string
): Promise<MarketplaceListingDoc> {
const res = await globalThis.fetch(
`${baseUrl}/marketplace/listings/${encodeURIComponent(id)}/submit`,
{
method: 'POST',
headers: headers(),
body: JSON.stringify({ notes }),
}
);
if (!res.ok) throw new Error(`submitForCertification failed: ${res.status}`);
return (await res.json()) as MarketplaceListingDoc;
}
// ── Installs ──────────────────────────────────────
async function installListing(listingId: string): Promise<MarketplaceInstallDoc> {
const res = await globalThis.fetch(
`${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/install`,
{
method: 'POST',
headers: headers(),
}
);
if (!res.ok) throw new Error(`installListing failed: ${res.status}`);
return (await res.json()) as MarketplaceInstallDoc;
}
async function uninstallListing(listingId: string): Promise<void> {
const res = await globalThis.fetch(
`${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/uninstall`,
{
method: 'POST',
headers: headers(),
}
);
if (!res.ok) throw new Error(`uninstallListing failed: ${res.status}`);
}
async function listMyInstalls(query?: {
limit?: number;
offset?: number;
}): Promise<MarketplaceInstallDoc[]> {
const params = new URLSearchParams();
if (query?.limit) params.set('limit', String(query.limit));
if (query?.offset) params.set('offset', String(query.offset));
const qs = params.toString();
const url = qs ? `${baseUrl}/marketplace/installs?${qs}` : `${baseUrl}/marketplace/installs`;
const res = await globalThis.fetch(url, { headers: headers() });
if (!res.ok) throw new Error(`listMyInstalls failed: ${res.status}`);
return (await res.json()) as MarketplaceInstallDoc[];
}
// ── Reviews ───────────────────────────────────────
async function listReviews(
listingId: string,
query?: { sortBy?: string; limit?: number }
): Promise<MarketplaceReviewDoc[]> {
const params = new URLSearchParams();
if (query?.sortBy) params.set('sortBy', query.sortBy);
if (query?.limit) params.set('limit', String(query.limit));
const qs = params.toString();
const url = qs
? `${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/reviews?${qs}`
: `${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/reviews`;
const res = await globalThis.fetch(url, { headers: headers() });
if (!res.ok) throw new Error(`listReviews failed: ${res.status}`);
return (await res.json()) as MarketplaceReviewDoc[];
}
async function createReview(
listingId: string,
input: { rating: number; title: string; body: string }
): Promise<MarketplaceReviewDoc> {
const res = await globalThis.fetch(
`${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/reviews`,
{
method: 'POST',
headers: headers(),
body: JSON.stringify(input),
}
);
if (!res.ok) throw new Error(`createReview failed: ${res.status}`);
return (await res.json()) as MarketplaceReviewDoc;
}
// ── Reports ───────────────────────────────────────
async function reportListing(
listingId: string,
input: { reason: string; details: string }
): Promise<void> {
const res = await globalThis.fetch(
`${baseUrl}/marketplace/listings/${encodeURIComponent(listingId)}/report`,
{
method: 'POST',
headers: headers(),
body: JSON.stringify(input),
}
);
if (!res.ok) throw new Error(`reportListing failed: ${res.status}`);
}
return {
listListings,
getListing,
createListing,
updateListing,
submitForCertification,
installListing,
uninstallListing,
listMyInstalls,
listReviews,
createReview,
reportListing,
};
}