# Shared @bytelyst/\* Client Packages — Extraction from Product Repos > **Status:** Packages Complete ✅ · All Product Migrations Complete ✅ > **Priority:** High — eliminates duplication across 9+ products > **Estimated effort:** 8–10 sessions > **Owner:** Cascade agent working in `learning_ai_common_plat` > > **Progress:** > > - ✅ All 9 packages built, tested (99 tests, 79/79 methods), committed (`be03efa`) > - ✅ NomGap web: all 9 packages wired — billing, celebrations, time-references, gentle-notifications, accessibility, quick-actions consumed (`abc13a5`) > - ✅ NomGap mobile: all 9 src/lib modules migrated to @bytelyst/\* wrappers (`77cbf60`) > - ✅ NomGap mobile: 5 migration bugs fixed + stale test updated (`9a18746`→`1c7ed45`) > - ✅ NomGap web: billing-client userId fallback fixed (`9934e13`) > - ✅ ChronoMind web: billing-client + feature-flags migrated (`f49ef78`) > - ✅ MindLyst web: billing-client + feature-flags migrated (`f110668`) > - ✅ LysnrAI user-dashboard: feature-flags migrated (`7539c65`) > - ✅ JarvisJr: audited — marketplace uses custom /catalog routes, no migration candidates > - ✅ FlowMonk, NoteLett, ActionTrail: already fully migrated (no hand-rolled duplicates) > - ✅ PeakPulse: iOS-only (no web app to migrate) --- ## Context & Motivation During NomGap (learning_ai_fastgap) Phase 5 buildout, we created several pure-TS engine modules that are **product-agnostic** and should live in `learning_ai_common_plat/packages/` as shared `@bytelyst/*` packages. The same patterns apply (or will apply) to all 9+ ByteLyst products: LysnrAI, MindLyst, ChronoMind, JarvisJr, NomGap, PeakPulse, FlowMonk, NoteLett, ActionTrail. Additionally, platform-service already has backend modules for referrals, subscriptions, plans, orgs, and licenses — but **no corresponding client packages** exist for products to consume them. Each product has been hand-rolling its own fetch calls or building bespoke modules. ### What already exists in platform-service (port 4003) > **IMPORTANT:** All platform-service endpoints require an `x-product-id` header (read by `getRequestProductId(req)`). Client packages MUST send this header on every request. | Backend Module | Exact Endpoints | Auth | Cosmos Container | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `modules/referrals/` | `GET /referrals`, `POST /referrals`, `PUT /referrals/:id`, `GET /referrals/stats`, `GET /referrals/by-referrer/:referrerId`, `GET /referrals/by-email/:email` | JWT | `referrals` (`/referrerId`) | | `modules/subscriptions/` | `GET /subscriptions/:userId`, `POST /subscriptions`, `PUT /subscriptions/:userId` (NOTE: keyed by userId, not id) | JWT | `subscriptions` (`/userId`) | | `modules/plans/` | `GET /plans` → `{ plans: [...] }`, `GET /plans/:name`, `POST /plans`, `PUT /plans/:id`, `POST /plans/seed` | JWT | `plans` (`/productId`) | | `modules/orgs/` | `GET/POST /orgs`, `GET/PATCH /orgs/:id`, workspaces: `GET/POST /orgs/:id/workspaces`, `PATCH /orgs/:id/workspaces/:workspaceId`, memberships: `GET/POST /orgs/:id/memberships`, `PATCH /orgs/:id/memberships/:membershipId` | **Admin-only** (`super_admin` or `admin` JWT role) | `organizations` (`/productId`), `workspaces` (`/orgId`), `memberships` (`/orgId`) | | `modules/licenses/` | Generate, activate, deactivate, list by user | JWT | `licenses` (`/userId`) | | `modules/marketplace/` | Listings CRUD + certification + reviews + installs + reports + votes (6 entity types, 309 lines of types) | JWT (some public) | `marketplace_listings` (`/productId`), `marketplace_reviews` (`/listingId`), `marketplace_installs` (`/userId`), etc. | ### What NomGap built as product-specific (should be shared) | NomGap File | What It Does | Should Become | | --------------------------------- | --------------------------------------------------------------------- | -------------------------------- | | `src/lib/referral.ts` | Referral code share links, apply codes, stats | `@bytelyst/referral-client` | | `src/lib/monetization.ts` | Plan definitions, Pro feature gate, trial, restore | `@bytelyst/monetization` | | `src/lib/b2b-wellness.ts` | Org dashboard, team challenges, licenses, SSO | `@bytelyst/org-client` | | `src/lib/influencer.ts` | Branded challenges, affiliate tracking, share cards | `@bytelyst/marketplace-client` | | `src/lib/celebrations.ts` | 14 celebration triggers, haptics, confetti, sounds, positive messages | `@bytelyst/celebrations` | | `src/lib/gentle-notifications.ts` | ND-friendly messaging, adaptive frequency, forbidden phrases | `@bytelyst/gentle-notifications` | | `src/lib/accessibility.ts` | VoiceOver/TalkBack label generators for common UI patterns | `@bytelyst/accessibility` | | `src/lib/quick-actions.ts` | Progressive disclosure, smart defaults, action definitions | `@bytelyst/quick-actions` | | `src/lib/time-blindness.ts` | Familiar duration references ("About as long as a movie") | `@bytelyst/time-references` | --- ## Packages to Create (Priority Order) ### Tier 1 — Highest Leverage (backend already exists, every product needs these) #### 1. `@bytelyst/referral-client` **Purpose:** Browser/React Native-safe client for platform-service `/referrals/*` endpoints. **Reference implementation:** `learning_ai_fastgap/src/lib/referral.ts` (73 lines) **Backend already exists:** `platform-service/src/modules/referrals/` — full CRUD + webhook dispatch + Zod schemas. **Backend types (from `referrals/types.ts`):** ```ts interface ReferralDoc { id: string; productId: string; referrerId: string; referrerEmail: string; referredUserId: string | null; referredEmail: string; status: 'pending' | 'signed_up' | 'subscribed' | 'rewarded'; referrerRewardTokens: number; referredRewardTokens: number; referrerRewarded: boolean; referredRewarded: boolean; createdAt: string; completedAt: string | null; } ``` **Public API to implement:** ```ts export interface ReferralClientConfig { baseUrl: string; // e.g. 'http://localhost:4003/api' productId: string; // sent as x-product-id header on every request getAccessToken: () => string | null; // Defaults used when creating referrals (so callers only provide referredEmail) defaultRewardTokens?: { referrer: number; referred: number }; // default: 1000 / 500 } export interface ReferralClient { // Core CRUD (delegates to platform-service) listMyReferrals(referrerId: string): Promise<{ referrals: ReferralDoc[]; count: number }>; getReferralStats(): Promise<{ total: number; completed: number; rewarded: number }>; createReferral(input: { referrerId: string; // from auth context referrerEmail: string; // from auth context referredEmail: string; // user input }): Promise; // NOTE: There is NO /referrals/apply endpoint in platform-service. // "Applying" a referral = calling PUT /referrals/:id to update status to 'signed_up'. // The client should expose this as a semantic method: updateReferralStatus( id: string, referrerId: string, status: ReferralDoc['status'] ): Promise; getByEmail(email: string): Promise; // Client-side helpers (pure TS, no network) buildShareLink(code: string): string; buildShareMessage(code: string, productName: string): string; calculateEarnedDays(conversions: number, daysPerReferral?: number): number; } export function createReferralClient(config: ReferralClientConfig): ReferralClient; ``` **⚠️ Backend gap:** Platform-service has no "apply referral code" endpoint. The flow for a referred user is: 1. `getByEmail(referredEmail)` → finds existing pending referral 2. `updateReferralStatus(id, referrerId, 'signed_up')` → marks as completed 3. Backend fires webhook via `dispatchReferralStatusChanged()` If a single "apply code" endpoint is needed, a new `POST /referrals/apply` route should be added to platform-service first. **Pattern:** Follow `@bytelyst/feature-flag-client` exactly — `createXxxClient()` factory, `types.ts` + `client.ts` + `index.ts`, `globalThis.fetch`, no Node.js deps. Every request MUST include `x-product-id` and `Authorization: Bearer ` headers. **Tests:** 8+ Vitest tests — create, listMyReferrals, stats, updateStatus, getByEmail, buildShareLink, buildShareMessage, calculateEarnedDays, error handling. --- #### 2. `@bytelyst/subscription-client` **Purpose:** Client for platform-service `/subscriptions/*` + `/plans/*` — check entitlements, manage trials, restore purchases. **Reference implementation:** `learning_ai_fastgap/src/lib/monetization.ts` (200 lines) **Backend already exists:** `platform-service/src/modules/subscriptions/` + `platform-service/src/modules/plans/` **Backend types (from `subscriptions/types.ts`):** ```ts interface SubscriptionDoc { id: string; productId: string; userId: string; plan: 'free' | 'pro' | 'enterprise'; status: 'active' | 'cancelled' | 'past_due' | 'trialing'; currentPeriodStart: string; currentPeriodEnd: string; cancelAtPeriodEnd: boolean; monthlyPrice: number; tokensIncluded: number; tokensUsed: number; stripeCustomerId?: string; stripeSubscriptionId?: string; createdAt: string; updatedAt: string; } ``` **Backend types (from `plans/types.ts`):** ```ts interface PlanConfig { id: string; productId: string; name: string; displayName: string; price: number; tokens: number; words: number; dictations: number; features: string[]; stripePriceId?: string; active: boolean; createdAt: string; updatedAt: string; } ``` **Public API to implement:** ```ts export interface SubscriptionClientConfig { baseUrl: string; productId: string; // sent as x-product-id header on every request userId: string; // REQUIRED — subscription routes are keyed by userId, not id getAccessToken: () => string | null; storage?: { getItem(k: string): string | null; setItem(k: string, v: string): void }; } export interface SubscriptionClient { // Server-authoritative checks (all hit platform-service) getMySubscription(): Promise; // NOTE: GET /plans returns { plans: [...] } — client must unwrap .plans getPlans(): Promise; startTrial(planName?: string): Promise; cancelSubscription(): Promise; // NOTE: PUT /subscriptions/:userId (not :id) — use config.userId updateSubscription(updates: Partial): Promise; // Client-side helpers (cached, offline-safe) isPro(): boolean; // cached: plan !== 'free' && status === 'active' | 'trialing' isTrialing(): boolean; // cached: status === 'trialing' hasFeature(feature: string): boolean; // cached: looks up PlanConfig.features.includes(feature) daysRemaining(): number | null; // cached: days until currentPeriodEnd getCachedSubscription(): SubscriptionDoc | null; getCachedPlans(): PlanConfig[]; refresh(): Promise; // re-fetch from server + update cache } export function createSubscriptionClient(config: SubscriptionClientConfig): SubscriptionClient; ``` **⚠️ Backend gaps & notes:** - **No restore-purchase endpoint** — Platform-service has no store receipt verification. `restorePurchase()` was removed from the client API. To support IAP restore, either: (a) add a `POST /subscriptions/restore` endpoint to platform-service that verifies Apple/Google receipts, or (b) products verify receipts client-side and call `updateSubscription()` with the new status. Recommend (a) as a future platform-service enhancement. - **`PlanConfig.features` is the entitlement source** — `hasFeature('ai_coaching')` checks `PlanConfig.features.includes('ai_coaching')` from the cached plan matching the user's subscription tier. Products define their own feature strings. - **`PlanConfig` fields `words`, `dictations`, `tokens`** are LysnrAI-specific legacy fields. Other products should ignore them and rely on `features: string[]` for entitlement checks. The `PlanConfig` type should be treated as extensible. - **Subscription routes use `:userId` not `:id`** — `GET /subscriptions/:userId` and `PUT /subscriptions/:userId`. The client must use `config.userId` for these calls. **Key design:** Cache subscription + plans in `storage` for offline reads. `isPro()`, `hasFeature()`, `isTrialing()` read from cache (never block on network). `refresh()` fetches from server and updates cache. **Tests:** 10+ Vitest tests — getMySubscription, getPlans (unwraps .plans), startTrial, isPro, hasFeature (with features array), isTrialing, daysRemaining, cache persistence, refresh, error handling. --- #### 3. `@bytelyst/celebrations` **Purpose:** Product-agnostic celebration engine — milestone triggers, haptic configs, confetti, sounds, positive reinforcement messages. **Reference implementation:** `learning_ai_fastgap/src/lib/celebrations.ts` (190 lines) **No backend needed** — pure client-side TS. **Public API to implement:** ```ts export type CelebrationTrigger = | 'task_completed' | 'streak_continued' | 'streak_milestone' | 'achievement_unlocked' | 'level_up' | 'personal_best' | 'milestone_reached' | 'goal_completed' | 'first_action' | 'halfway' | 'session_completed' | 'session_started'; export interface Celebration { id: string; title: string; body: string; emoji: string; hapticType: 'light' | 'medium' | 'heavy' | 'success' | 'warning'; confetti: boolean; sound: 'chime' | 'success' | 'level_up' | 'none'; } export interface CelebrationConfig { // Products register their own trigger→message mappings customTriggers?: Record; } export function createCelebrationEngine(config?: CelebrationConfig): { getCelebration(trigger: CelebrationTrigger | string): Celebration; getTimedCelebrations(elapsedMs: number, targetMs: number, shownIds: Set): Celebration[]; isPersonalBest(current: number, previous: number): boolean; getPositiveMessage(progressPercent: number): string; // For early breaks / incomplete actions — NEVER negative getPositiveIncompleteMessage(progressPercent: number): string; }; ``` **Key design:** Products register custom triggers via `customTriggers` config. Default triggers are universal (streaks, achievements, levels). Messages are ALWAYS positive — never guilt-inducing. **Tests:** 8+ tests — all trigger types, custom triggers, timed celebrations, personal best, positive messages, incomplete messages. --- #### 4. `@bytelyst/gentle-notifications` **Purpose:** Neurodivergent-friendly notification messaging system — encouraging tone, adaptive frequency, forbidden phrases. **Reference implementation:** `learning_ai_fastgap/src/lib/gentle-notifications.ts` (120 lines) **No backend needed** — pure client-side TS. **Public API to implement:** ```ts export interface GentleNotificationConfig { maxPerHour: number; tone: 'encouraging' | 'neutral' | 'minimal'; adaptiveFrequency: boolean; dismissCount: number; suppressThreshold: number; } export interface GentleMessage { title: string; body: string; tone: 'encouraging' | 'neutral' | 'minimal'; } export function createGentleNotificationEngine(config?: Partial): { getDefaultConfig(): GentleNotificationConfig; getMessage(type: string, config?: GentleNotificationConfig): GentleMessage; shouldSuppress(config: GentleNotificationConfig): boolean; recordDismissal(config: GentleNotificationConfig): GentleNotificationConfig; resetDismissals(config: GentleNotificationConfig): GentleNotificationConfig; // Products register their own message pools registerMessages(type: string, messages: GentleMessage[]): void; // Ecosystem-wide forbidden phrases — NEVER use these in any notification getForbiddenPhrases(): readonly string[]; containsForbiddenPhrase(text: string): boolean; }; // Exported constant for lint rules / CI checks export const FORBIDDEN_PHRASES: readonly string[]; ``` **Forbidden phrases (ecosystem-wide policy):** ```ts [ "You haven't", 'You forgot', "Don't forget", 'You should have', "Why didn't you", 'You missed', 'You failed', 'You need to', ]; ``` **Key design:** Products register their own message pools via `registerMessages()`. The forbidden phrases list is an ecosystem-wide policy enforced across all products. `containsForbiddenPhrase()` can be used in CI/lint. **Tests:** 8+ tests — getMessage, shouldSuppress, recordDismissal, resetDismissals, registerMessages, forbiddenPhrases, containsForbiddenPhrase, adaptive frequency reduction. --- #### 5. `@bytelyst/accessibility` **Purpose:** VoiceOver/TalkBack/Dynamic Type accessibility label generators for common UI patterns used across all products. **Reference implementation:** `learning_ai_fastgap/src/lib/accessibility.ts` (220 lines) **No backend needed** — pure client-side TS. **Public API to implement:** ```ts export interface A11yProps { accessible: boolean; accessibilityLabel: string; accessibilityHint?: string; accessibilityRole?: | 'button' | 'header' | 'text' | 'timer' | 'progressbar' | 'image' | 'alert' | 'summary' | 'adjustable'; accessibilityState?: { disabled?: boolean; selected?: boolean; checked?: boolean; busy?: boolean; expanded?: boolean; }; accessibilityValue?: { min?: number; max?: number; now?: number; text?: string }; } // Common UI pattern label generators export function buttonLabel(label: string, hint?: string): A11yProps; export function timerLabel(status: string, elapsedText: string, context?: string): A11yProps; export function progressLabel(name: string, percent: number, description?: string): A11yProps; export function sliderLabel(metric: string, value: number, max?: number): A11yProps; export function alertLabel(severity: string, message: string): A11yProps; export function achievementLabel(name: string, description: string, earned: boolean): A11yProps; export function streakLabel(current: number, longest: number): A11yProps; export function listItemLabel(title: string, subtitle?: string, badge?: string): A11yProps; // Utilities export function formatDurationForA11y(ms: number): string; // "16 hours 30 minutes" export function formatNumberForA11y(n: number): string; // "1,234" → "one thousand two hundred thirty four" export function buildAnnouncement(headline: string, detail: string): string; // Positive messaging (never negative, complements celebrations package) export function getPositiveBreakMessage(progressPercent: number): string; ``` **Key design:** React Native `accessibilityLabel` / `accessibilityRole` compatible. Web apps can map to `aria-label` / `role`. Every label function returns a complete `A11yProps` object ready to spread onto a component. **Tests:** 10+ tests — each label generator, formatDurationForA11y, formatNumberForA11y, buildAnnouncement, positiveBreakMessage, edge cases (0%, 100%, negative). --- ### Tier 2 — High Value (no backend dependency) #### 6. `@bytelyst/quick-actions` **Purpose:** Progressive disclosure system, smart defaults, quick action definitions for reducing cognitive load. **Reference implementation:** `learning_ai_fastgap/src/lib/quick-actions.ts` (170 lines) **Public API to implement:** ```ts export interface QuickAction { id: string; label: string; icon: string; shortLabel: string; action: string; requiresAuth: boolean; } export interface ProgressiveSection { id: string; title: string; defaultExpanded: boolean; priority: 'primary' | 'secondary' | 'detail'; } export interface SmartDefault { key: string; value: unknown; source: 'last_used' | 'most_common' | 'recommendation' | 'system'; } export function getVisibleSections( sections: ProgressiveSection[], expandedIds: Set ): ProgressiveSection[]; export function getAvailableActions( actions: QuickAction[], context: { isActive?: boolean; isAuthenticated?: boolean } ): QuickAction[]; export function pickSmartDefault(candidates: SmartDefault[]): SmartDefault | null; export const MAX_VISIBLE_ITEMS = 3; export const MAX_VISIBLE_LIST = 5; ``` **Tests:** 6+ tests. --- #### 7. `@bytelyst/time-references` **Purpose:** Familiar duration references for time-blindness aids ("About as long as a movie", "3 episodes of The Office"). **Reference implementation:** `learning_ai_fastgap/src/lib/time-blindness.ts` (175 lines) **Public API to implement:** ```ts export interface TimeReference { text: string; emoji: string; category: 'media' | 'activity' | 'travel' | 'nature'; } export function getTimeReference(elapsedHours: number): TimeReference; export function getEpisodeComparison( elapsedHours: number, showName?: string, episodeMins?: number ): string; export function getEncouragingMessage(elapsedHours: number): string; // Products can register custom reference databases export function registerReferences( entries: Array<{ minHours: number; maxHours: number; references: TimeReference[] }> ): void; ``` **Tests:** 6+ tests. --- ### Tier 3 — Backend Client Wrappers #### 8. `@bytelyst/org-client` **Purpose:** Client for platform-service `/orgs/*` + `/licenses/*` — B2B org management, workspace management, membership management. **Reference implementation:** `learning_ai_fastgap/src/lib/b2b-wellness.ts` (100 lines) — but the actual backend is far richer. **Backend already exists:** `platform-service/src/modules/orgs/` (3 entity types) + `platform-service/src/modules/licenses/` **⚠️ Auth requirement:** All org routes require **admin-only** access (`super_admin` or `admin` JWT role via `requireAdmin(req)`). Regular user tokens will get `403 Forbidden`. **Backend types (from `orgs/types.ts`):** ```ts interface OrganizationDoc { id: string; // org_ productId: string; name: string; slug: string; status: 'active' | 'disabled'; ownerUserId: string; metadata?: Record; createdAt: string; updatedAt: string; } interface WorkspaceDoc { id: string; // ws_ orgId: string; productId: string; name: string; slug: string; status: 'active' | 'archived'; description?: string; metadata?: Record; createdAt: string; updatedAt: string; } interface MembershipDoc { id: string; // mbr___ orgId: string; productId: string; scope: 'org' | 'workspace'; workspaceId?: string; userId: string; role: 'owner' | 'admin' | 'member' | 'viewer'; status: 'active' | 'invited' | 'disabled'; invitedBy?: string; createdAt: string; updatedAt: string; } interface LicenseDoc { id: string; productId: string; key: string; // LYSNR-XXXX-XXXX-XXXX format userId: string; plan: 'free' | 'pro' | 'enterprise'; status: 'active' | 'revoked' | 'expired'; activatedAt: string | null; expiresAt: string | null; deviceIds: string[]; maxDevices: number; createdAt: string; updatedAt: string; } ``` **Public API to implement:** ```ts export interface OrgClientConfig { baseUrl: string; productId: string; getAccessToken: () => string | null; } export interface OrgClient { // Organizations listOrgs(query?: { status?: string; limit?: number }): Promise; createOrg(input: { name: string; slug: string; ownerUserId?: string }): Promise; getOrg(id: string): Promise; updateOrg(id: string, updates: Partial): Promise; // Workspaces listWorkspaces(orgId: string): Promise; createWorkspace( orgId: string, input: { name: string; slug: string; description?: string } ): Promise; updateWorkspace( orgId: string, workspaceId: string, updates: Partial ): Promise; // Memberships listMemberships( orgId: string, query?: { scope?: string; limit?: number } ): Promise; addMember( orgId: string, input: { userId: string; role?: string; scope?: string; workspaceId?: string } ): Promise; updateMember( orgId: string, membershipId: string, updates: { role?: string; status?: string } ): Promise; // Licenses generateLicense(input: { userId: string; plan: string; maxDevices?: number; }): Promise; activateLicense(input: { key: string; deviceId: string }): Promise; deactivateLicense(input: { key: string; deviceId: string }): Promise; } export function createOrgClient(config: OrgClientConfig): OrgClient; ``` **Tests:** 10+ Vitest tests — listOrgs, createOrg, getOrg, listWorkspaces, createWorkspace, listMemberships, addMember, generateLicense, activateLicense, error handling (403 for non-admin). --- #### 9. `@bytelyst/marketplace-client` **Purpose:** Client for platform-service `/marketplace/*` — template marketplace with listings, reviews, installs, certification, reports. **Reference implementation:** `learning_ai_fastgap/src/lib/influencer.ts` (95 lines) covers a subset. The actual backend is a full marketplace with 6 entity types. **Backend already exists:** `platform-service/src/modules/marketplace/` — 309 lines of types, 6 entity types. **⚠️ Note:** NomGap's `influencer.ts` (branded challenges, affiliate tracking) is a **product-specific layer** on top of the marketplace. The shared client should expose the generic marketplace API. Products build influencer/branded features on top. **Backend types (key entities from `marketplace/types.ts`):** ```ts interface MarketplaceListingDoc { id: string; // lst_ productId: string; templateType: string; // product-specific: 'fasting_protocol', 'agent_persona', 'timer_preset', etc. authorId: string; authorName: string; title: string; shortDescription: string; description: string; tags: string[]; category: string; payload: Record; // product-specific JSON pricingModel: 'free' | 'paid' | 'freemium'; priceInCents: number; certificationStatus: 'draft' | 'submitted' | 'in_review' | 'approved' | 'rejected' | 'suspended'; installCount: number; reviewCount: number; averageRating: number; visibility: 'private' | 'unlisted' | 'public'; featured: boolean; version: string; createdAt: string; updatedAt: string; } interface MarketplaceReviewDoc { id: string; // rev_ listingId: string; productId: string; authorId: string; rating: number; // 1-5 title: string; body: string; verified: boolean; createdAt: string; } interface MarketplaceInstallDoc { id: string; // inst_ listingId: string; productId: string; userId: string; version: string; installedAt: string; uninstalledAt: string | null; } ``` **Public API to implement:** ```ts export interface MarketplaceClientConfig { baseUrl: string; productId: string; getAccessToken: () => string | null; } export interface MarketplaceClient { // Listings listListings(query?: { templateType?: string; category?: string; tags?: string; pricingModel?: string; sortBy?: string; q?: string; limit?: number; offset?: number; }): Promise<{ listings: MarketplaceListingDoc[]; total: number }>; getListing(id: string): Promise; createListing(input: CreateListingInput): Promise; updateListing( id: string, updates: Partial ): Promise; submitForCertification(id: string, notes?: string): Promise; // Installs installListing(listingId: string): Promise; uninstallListing(listingId: string): Promise; listMyInstalls(query?: { limit?: number; offset?: number }): Promise; // Reviews listReviews( listingId: string, query?: { sortBy?: string; limit?: number } ): Promise; createReview( listingId: string, input: { rating: number; title: string; body: string } ): Promise; // Reports reportListing(listingId: string, input: { reason: string; details: string }): Promise; } export function createMarketplaceClient(config: MarketplaceClientConfig): MarketplaceClient; ``` **Tests:** 10+ Vitest tests — listListings (with filters), getListing, createListing, submitForCertification, installListing, listMyInstalls, listReviews, createReview, reportListing, error handling. --- ## Implementation Instructions ### For each package, follow this exact pattern: ``` packages// ├── package.json # @bytelyst/, version 0.1.0, type: module ├── tsconfig.json # extends ../../tsconfig.base.json ├── vitest.config.ts # (optional) only if tests need special config; root config handles most cases ├── src/ │ ├── index.ts # re-exports from client.ts + types.ts │ ├── types.ts # all interfaces + config types │ └── client.ts # createXxxClient() factory or pure functions └── tests/ └── client.test.ts # Vitest tests ``` ### package.json template: ```json { "name": "@bytelyst/", "version": "0.1.0", "type": "module", "description": "", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": ["dist"], "scripts": { "build": "tsc", "test": "vitest run" } } ``` > **Note on devDependencies:** `typescript` and `vitest` are provided by the pnpm workspace root and do NOT need to be listed as devDependencies in individual packages. The root `vitest.config.ts` (with `passWithNoTests: true`) handles all packages. Only add a per-package `vitest.config.ts` if tests need special setup (e.g., custom globals or test environment). ### tsconfig.json template: ```json { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", "rootDir": "src" }, "include": ["src"] } ``` ### Conventions (MUST follow): - **ESM everywhere:** `"type": "module"`, `.js` extensions in imports - **No Node.js deps:** Use `globalThis.fetch`, not node-fetch - **No React/RN deps:** Pure TS only — consumed by web, mobile, Node.js - **Factory pattern** for API clients: `createXxxClient(config)` returning an interface - **Storage optional:** Accept `{ getItem, setItem }` for cache/persistence - **Vitest tests:** Every public function must have at least one test - **No `console.log`** — silent by default - **No `any` type** — use explicit types or generics ### After creating packages: 1. Add each to `pnpm-workspace.yaml` (already covered by `packages/*` glob) 2. Run `pnpm build` to verify all compile 3. Run `pnpm test` to verify all tests pass 4. Run `pnpm typecheck` to verify no type errors ### After packages are built, migrate product repos: For each product (NomGap first, then others): 1. Add `"@bytelyst/": "file:../../learning_ai_common_plat/packages/"` to product's `package.json` 2. Replace product-specific module with thin wrapper that delegates to shared package 3. Remove duplicated code 4. Run product's `npm test && npm run typecheck` --- ## Execution Order | # | Package | Tier | Est. Lines | Backend Dep | Tests | | --------- | -------------------------------- | ---- | ---------- | -------------------------------------- | ------ | | 1 | `@bytelyst/referral-client` | 1 | ~150 | Yes (referrals) | 8 | | 2 | `@bytelyst/subscription-client` | 1 | ~220 | Yes (subscriptions + plans) | 10 | | 3 | `@bytelyst/celebrations` | 1 | ~150 | No | 8 | | 4 | `@bytelyst/gentle-notifications` | 1 | ~130 | No | 8 | | 5 | `@bytelyst/accessibility` | 1 | ~200 | No | 10 | | 6 | `@bytelyst/quick-actions` | 2 | ~120 | No | 6 | | 7 | `@bytelyst/time-references` | 2 | ~150 | No | 6 | | 8 | `@bytelyst/org-client` | 3 | ~250 | Yes (orgs + licenses) — **admin-only** | 10 | | 9 | `@bytelyst/marketplace-client` | 3 | ~200 | Yes (marketplace — 6 entity types) | 10 | | **Total** | | | **~1,570** | | **76** | --- ## Verification After all packages are created: ```bash cd learning_ai_common_plat pnpm build && pnpm test && pnpm typecheck ``` After NomGap migration: ```bash cd learning_ai_fastgap npm test && npm run typecheck ``` --- ## Products That Will Consume These Packages > ✅ = immediate consumer, ⭐ = primary driver (has existing code to migrate), — = not needed initially (can adopt later) | Package | NomGap | LysnrAI | MindLyst | ChronoMind | JarvisJr | PeakPulse | FlowMonk | NoteLett | ActionTrail | | -------------------- | ------ | ------- | -------- | ---------- | -------- | --------- | -------- | -------- | ----------- | | referral-client | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | subscription-client | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | celebrations | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | gentle-notifications | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | accessibility | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | quick-actions | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | time-references | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | org-client | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | marketplace-client | ✅ | — | — | — | ⭐ | — | — | ✅ | — | > **Notes on matrix:** > > - `time-references` is useful for ANY app with timers, sessions, streaks, or progress tracking — not just fasting apps. Products customize the reference database via `registerReferences()`. > - `org-client` is relevant for all products if they offer a B2B tier. Initially only products with active B2B plans need it. > - `marketplace-client` is currently relevant for NomGap (fasting protocols) and JarvisJr (agent personas). Other products can adopt it when they add marketplace features. --- ## NomGap Migration Plan (First Product) After the shared packages are built, NomGap (`learning_ai_fastgap`) should be migrated first since it has the most existing code to replace. ### Files to replace in NomGap: | NomGap File | Replace With | Action | | --------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------- | | `src/lib/referral.ts` (73 lines) | `@bytelyst/referral-client` | Rewrite as thin wrapper: `import { createReferralClient } from '@bytelyst/referral-client'` | | `src/lib/monetization.ts` (200 lines) | `@bytelyst/subscription-client` | Rewrite: `createSubscriptionClient(config)` + product-specific plan names | | `src/lib/b2b-wellness.ts` (100 lines) | `@bytelyst/org-client` | Rewrite: `createOrgClient(config)` + NomGap-specific wellness types on top | | `src/lib/influencer.ts` (95 lines) | `@bytelyst/marketplace-client` | Rewrite: `createMarketplaceClient(config)` + influencer layer on top | | `src/lib/celebrations.ts` (190 lines) | `@bytelyst/celebrations` | Rewrite: `createCelebrationEngine({ customTriggers: nomgapTriggers })` | | `src/lib/gentle-notifications.ts` (120 lines) | `@bytelyst/gentle-notifications` | Rewrite: `createGentleNotificationEngine()` + `registerMessages('fasting', [...])` | | `src/lib/accessibility.ts` (220 lines) | `@bytelyst/accessibility` | Rewrite: import shared label generators + NomGap-specific labels | | `src/lib/quick-actions.ts` (170 lines) | `@bytelyst/quick-actions` | Rewrite: import shared functions + NomGap-specific action/section defs | | `src/lib/time-blindness.ts` (175 lines) | `@bytelyst/time-references` | Rewrite: `import { getTimeReference } from '@bytelyst/time-references'` | ### Steps: 1. Add `file:` deps to `package.json`: `"@bytelyst/referral-client": "file:../../learning_ai_common_plat/packages/referral-client"`, etc. 2. For each file, replace the standalone implementation with a thin wrapper that delegates to the shared package 3. Keep any NomGap-specific logic (fasting-specific celebrations, meal-specific suggestions) in the product repo 4. Run `npm test && npm run typecheck` after each migration 5. Total estimated reduction: ~1,340 lines of NomGap-specific code replaced with ~300 lines of thin wrappers ### Other product migrations: After NomGap is migrated, other products can adopt packages as needed. Each product adds `file:` deps and creates thin wrappers. No product needs to migrate all 9 packages at once — adopt incrementally. --- ## Known Backend Gaps (Require Platform-Service Changes) These gaps were identified during the review. They should be addressed in platform-service BEFORE or IN PARALLEL with the client packages: | Gap | Impact | Recommendation | | -------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- | | No `POST /referrals/apply` endpoint | Referred users can't "apply" a code in one step | Add a `POST /referrals/apply` route that finds by email + updates status atomically | | No `POST /subscriptions/restore` endpoint | IAP restore requires receipt verification | Add a store receipt verification endpoint (Apple/Google JWKS) | | `PlanConfig` has LysnrAI-specific fields (`words`, `dictations`, `tokens`) | Other products ignore these fields | Consider making plan schema extensible with `metadata: Record` | | Org routes require admin JWT role | Regular users can't view their own org | Consider adding user-facing org endpoints (e.g., `GET /my/org`) |