# 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<ReferralDoc>;
  // 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<ReferralDoc>;
  getByEmail(email: string): Promise<ReferralDoc | null>;

  // 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 <token>` 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<SubscriptionDoc | null>;
  // NOTE: GET /plans returns { plans: [...] } — client must unwrap .plans
  getPlans(): Promise<PlanConfig[]>;
  startTrial(planName?: string): Promise<SubscriptionDoc>;
  cancelSubscription(): Promise<SubscriptionDoc>;
  // NOTE: PUT /subscriptions/:userId (not :id) — use config.userId
  updateSubscription(updates: Partial<SubscriptionDoc>): Promise<SubscriptionDoc>;

  // 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<void>; // 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<string, Celebration>;
}

export function createCelebrationEngine(config?: CelebrationConfig): {
  getCelebration(trigger: CelebrationTrigger | string): Celebration;
  getTimedCelebrations(elapsedMs: number, targetMs: number, shownIds: Set<string>): 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<GentleNotificationConfig>): {
  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<string>
): 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_<uuid>
  productId: string;
  name: string;
  slug: string;
  status: 'active' | 'disabled';
  ownerUserId: string;
  metadata?: Record<string, unknown>;
  createdAt: string;
  updatedAt: string;
}

interface WorkspaceDoc {
  id: string; // ws_<uuid>
  orgId: string;
  productId: string;
  name: string;
  slug: string;
  status: 'active' | 'archived';
  description?: string;
  metadata?: Record<string, unknown>;
  createdAt: string;
  updatedAt: string;
}

interface MembershipDoc {
  id: string; // mbr_<orgId>_<userId>_<scope>
  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<OrganizationDoc[]>;
  createOrg(input: { name: string; slug: string; ownerUserId?: string }): Promise<OrganizationDoc>;
  getOrg(id: string): Promise<OrganizationDoc>;
  updateOrg(id: string, updates: Partial<OrganizationDoc>): Promise<OrganizationDoc>;

  // Workspaces
  listWorkspaces(orgId: string): Promise<WorkspaceDoc[]>;
  createWorkspace(
    orgId: string,
    input: { name: string; slug: string; description?: string }
  ): Promise<WorkspaceDoc>;
  updateWorkspace(
    orgId: string,
    workspaceId: string,
    updates: Partial<WorkspaceDoc>
  ): Promise<WorkspaceDoc>;

  // Memberships
  listMemberships(
    orgId: string,
    query?: { scope?: string; limit?: number }
  ): Promise<MembershipDoc[]>;
  addMember(
    orgId: string,
    input: { userId: string; role?: string; scope?: string; workspaceId?: string }
  ): Promise<MembershipDoc>;
  updateMember(
    orgId: string,
    membershipId: string,
    updates: { role?: string; status?: string }
  ): Promise<MembershipDoc>;

  // Licenses
  generateLicense(input: {
    userId: string;
    plan: string;
    maxDevices?: number;
  }): Promise<LicenseDoc>;
  activateLicense(input: { key: string; deviceId: string }): Promise<LicenseDoc>;
  deactivateLicense(input: { key: string; deviceId: string }): Promise<void>;
}

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_<uuid>
  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<string, unknown>; // 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_<uuid>
  listingId: string;
  productId: string;
  authorId: string;
  rating: number; // 1-5
  title: string;
  body: string;
  verified: boolean;
  createdAt: string;
}

interface MarketplaceInstallDoc {
  id: string; // inst_<uuid>
  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<MarketplaceListingDoc>;
  createListing(input: CreateListingInput): Promise<MarketplaceListingDoc>;
  updateListing(
    id: string,
    updates: Partial<MarketplaceListingDoc>
  ): Promise<MarketplaceListingDoc>;
  submitForCertification(id: string, notes?: string): Promise<MarketplaceListingDoc>;

  // Installs
  installListing(listingId: string): Promise<MarketplaceInstallDoc>;
  uninstallListing(listingId: string): Promise<void>;
  listMyInstalls(query?: { limit?: number; offset?: number }): Promise<MarketplaceInstallDoc[]>;

  // Reviews
  listReviews(
    listingId: string,
    query?: { sortBy?: string; limit?: number }
  ): Promise<MarketplaceReviewDoc[]>;
  createReview(
    listingId: string,
    input: { rating: number; title: string; body: string }
  ): Promise<MarketplaceReviewDoc>;

  // Reports
  reportListing(listingId: string, input: { reason: string; details: string }): Promise<void>;
}

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-name>/
├── package.json          # @bytelyst/<name>, 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/<package-name>",
  "version": "0.1.0",
  "type": "module",
  "description": "<one-line 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/<package>": "file:../../learning_ai_common_plat/packages/<package>"` 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<string, unknown>`     |
| 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`)                     |
