// ============================================================================ // @boe-ventures/platform-schemas // ============================================================================ // // TypeScript schemas for unofficial messaging platform APIs. // // Each platform module documents: // 1. API endpoints (URLs, methods, required headers) // 2. Authentication pattern (how to extract tokens from a browser session) // 3. Raw response types (what the API actually returns) // 4. Normalization functions (raw → Conversation) // 5. Rate limiting / anti-detection notes // 6. Known quirks and version history // // The goal: when LinkedIn changes their GraphQL query IDs, someone opens a PR, // bumps the version, and every consumer picks it up. // ============================================================================ // ── Normalized Types ────────────────────────────────────────────────────── /** Unique platform identifier */ export type Platform = | "slack" | "linkedin" | "instagram" | "tinder" | "whatsapp" | "x" | "imessage" | "homi"; /** Conversation type — explicit, not inferred */ export type ConversationType = "dm" | "group_dm" | "channel" | "thread"; /** * Content type of a message. Wider than the original set so we can represent * the richness real platforms carry (voice notes, stickers, locations, etc.). * The actual bytes/URLs live in {@link Attachment}; this is the headline shape. */ export type MessageType = | "text" | "image" | "video" | "audio" | "file" | "link" | "location" | "contact" | "sticker" | "reaction" | "system" | "other"; /** Per-message delivery state (sender's view). */ export type DeliveryStatus = | "pending" | "sent" | "delivered" | "read" | "played" | "failed"; /** A participant's role within a group conversation. */ export type ParticipantRole = "member" | "admin" | "owner"; /** An emoji reaction on a message. Platforms deliver the full current set. */ export interface Reaction { /** Normalized unicode emoji or shortcode (e.g. "👍" or ":+1:") */ emoji: string; /** Who reacted (platform-specific id) */ senderId: string; /** Reactor display name, denormalized for convenience */ senderName?: string; /** ISO 8601 when the reaction was added */ timestamp?: string; } /** An @-mention inside a message body. */ export interface Mention { /** Platform-specific id of the mentioned participant */ participantId: string; /** Char offset into Message.text, when the platform provides it */ offset?: number; /** Length of the mention span in chars */ length?: number; } /** A media/file/link attachment on a message. */ export interface Attachment { type: "image" | "file" | "link" | "video" | "audio" | "sticker" | "voice"; /** Resolvable URL, OR a sidecar cache ref ("hydra-media://") for lazy media */ url: string; name?: string; mime?: string; width?: number; height?: number; /** Duration for audio/video in seconds */ durationSec?: number; sizeBytes?: number; /** Inline base64 data URI (e.g. WhatsApp jpegThumbnail) for instant render */ thumbnail?: string; /** True until the full media is downloaded on demand */ pending?: boolean; } /** * A person's handle within a conversation — how one Person appears on one * platform. The desktop hub resolves Participants into unified People via their * join keys (email, phone, platform+username). See {@link Person}. */ export interface Participant { /** Platform-specific user ID */ id: string; /** Display name */ name: string; /** Platform username/handle (e.g. @kristianeboe) */ username?: string; /** Avatar URL */ avatarUrl?: string; /** Link to their profile on the platform */ profileUrl?: string; /** Email if known (for identity resolution) */ email?: string; /** Phone if known (for identity resolution) */ phone?: string; /** Role within a group conversation (WhatsApp/Slack admins, owners) */ role?: ParticipantRole; /** ISO 8601 when they joined the group, if known */ joinedAt?: string; /** True if this participant is the connected account (you) */ isSelf?: boolean; } /** A single message */ export interface Message { /** Platform-specific message ID */ id: string; /** Sender's platform-specific ID */ senderId: string; /** Sender's display name (denormalized for convenience) */ senderName?: string; /** Message text content */ text: string; /** ISO 8601 timestamp */ timestamp: string; /** Content type */ type: MessageType; /** For threaded messages (Slack) — parent/root message ID */ threadId?: string; /** True if the connected account (you) sent this — drives alignment + hasReplied */ isFromMe?: boolean; /** Quote/reply target — the message this one replies to (WhatsApp, iMessage) */ replyTo?: { messageId: string; senderId?: string; /** Short preview of the quoted message, denormalized for render */ textPreview?: string; }; /** Current reaction set (full state — overwrite on update, don't append) */ reactions?: Reaction[]; /** @-mentions in the body */ mentions?: Mention[]; /** ISO 8601 if this message was edited */ editedAt?: string; /** ISO 8601 tombstone if revoked/deleted (text cleared, row kept) */ deletedAt?: string; /** Delivery state from the sender's perspective */ deliveryStatus?: DeliveryStatus; /** Attachments (media/files/links) */ attachments?: Attachment[]; /** Non-queried structured extras rendered per-platform (forwarded flag, ttl, …) */ platformMeta?: Record; /** Full original platform payload — escape hatch for re-normalization. NEVER rendered. */ raw?: unknown; } /** A connected account on a platform */ export interface Account { /** Stable ID: platform:username or platform:workspaceId */ id: string; platform: Platform; /** Platform username/handle */ username?: string; /** Display name for the UI */ displayName?: string; /** User-editable label ("Work IG", "Personal") */ label?: string; /** Category tag for filtering */ category?: "work" | "personal" | "side-project" | string; /** Slack workspace / team ID */ workspaceId?: string; /** Slack workspace name */ workspaceName?: string; /** Profile URL */ profileUrl?: string; /** Avatar URL */ avatarUrl?: string; /** Platform-specific user ID */ platformUserId?: string; /** Chrome profile this came from */ chromeProfileId?: string; /** When this account was first seen (ISO 8601) */ connectedAt?: string; /** Whether active */ enabled: boolean; /** * How this account's data is sourced. Default "session" — the extension riding * your browser session (local-first). "zernio" is reserved for a future * bring-your-own-API-key path; "manual" for hand-added accounts. Declared so the * model has a home for it — not built yet. */ source?: "session" | "zernio" | "manual"; } /** Generate a stable account ID */ export function makeAccountId(platform: Platform, identity: string): string { return `${platform}:${identity}`; } /** A normalized conversation across any platform */ export interface Conversation { /** Platform this conversation belongs to */ platform: Platform; /** Which account this conversation belongs to (set by the caller, not the normalizer) */ accountId?: string; /** Platform-specific conversation ID */ conversationId: string; /** Explicit conversation type */ conversationType: ConversationType; /** Conversation name (channel name, or synthesized from participants) */ name?: string; /** People in this conversation */ participants: Participant[]; /** Most recent message (quick preview without fetching full messages) */ lastMessage: Message | null; /** Recent messages (fetched separately for active threads) */ messages?: Message[]; /** ISO 8601 timestamp of last activity */ lastActivityAt: string; /** Platform-reported unread count */ unreadCount: number; /** Whether the current user has sent a message in this conversation */ hasReplied?: boolean; /** Group subject/topic (WhatsApp group subject; distinct from a synthesized DM name) */ subject?: string; /** Group avatar/photo URL */ avatarUrl?: string; /** ISO 8601 when the group was created */ createdAt?: string; /** Group owner/creator participant id */ ownerId?: string; /** False once the connected account has left/been removed from the group */ isActive?: boolean; /** Platform-specific metadata (team ID, thread URL, etc.) */ platformMeta?: Record; } // ── Identity / People ────────────────────────────────────────────────────── // // The three nouns of the shared model: // Platform — the service (instagram, linkedin, …) // Account — YOUR identity on a platform (you may have several) // Person — someone you talk TO; a unified contact, the same human across // platforms. A Person has one PersonHandle per platform identity. // // People + PersonHandles are RESOLVED AND OWNED BY THE DESKTOP HUB. They do NOT // flow over the extension→desktop bridge — the extension emits Participants // (handles), the hub unifies them into People. Defined here so every consumer // (desktop, MCP, an agent, a future web client) speaks one vocabulary. /** A unified contact — the same human across platforms. */ export interface Person { /** Stable hub-assigned id */ id: string; /** Best-known display name */ displayName: string; /** Known emails — identity join keys */ emails?: string[]; /** Known phone numbers — identity join keys */ phones?: string[]; /** Free-form notes / short-to-medium-term agent memory about this person */ notes?: string; /** The platform handles that resolve to this person */ handles?: PersonHandle[]; } /** * One platform identity belonging to a {@link Person} — their "profile" on a * single platform. This is the resolved form of a {@link Participant}. */ export interface PersonHandle { /** Stable hub-assigned id */ id: string; /** The unified Person this handle belongs to */ personId: string; platform: Platform; /** Which of YOUR accounts saw this handle (optional) */ accountId?: string; /** Platform-specific user id */ platformUserId?: string; /** @handle / username on this platform */ username?: string; /** Display name as seen on this platform */ displayName?: string; avatarUrl?: string; profileUrl?: string; } // ── Platform Adapter Interface ──────────────────────────────────────────── /** Authentication tokens extracted from a browser session */ export interface PlatformAuth { /** Platform this auth belongs to */ platform: Platform; /** Named tokens (e.g. { csrfToken: "...", cookie: "..." }) */ tokens: Record; /** When these tokens were extracted */ extractedAt: string; /** Platform-specific metadata (team ID, workspace, profile URN) */ meta?: Record; } /** API endpoint definition */ export interface ApiEndpoint { /** Human-readable name for this endpoint */ name: string; /** HTTP method */ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; /** URL template — use {placeholders} for dynamic parts */ urlTemplate: string; /** Required headers (values can reference auth tokens via {tokenName}) */ headers: Record; /** Request body template for POST/PUT (if applicable) */ bodyTemplate?: unknown; /** What this endpoint returns */ description: string; /** * Rate limiting notes — what we know about limits and detection. * This is critical knowledge for avoiding bans. */ rateLimiting?: string; /** * Agent/human confidence metadata. This package is a field manual, not a * guarantee that every endpoint is currently exercised by Hydra runtime code. */ verification?: EndpointVerification; } export type EndpointVerificationStatus = | "live_verified" | "documented" | "experimental" | "reference_only" | "stale"; export type EndpointVerificationSource = | "hydra_runtime" | "hydra_har" | "manual_capture" | "external_reference" | "local_platform"; export interface EndpointVerification { /** How much trust a human or agent should place in this endpoint today. */ status: EndpointVerificationStatus; /** Last date this claim was checked or deliberately updated. */ lastChecked: string; /** Where the current claim comes from. */ source: EndpointVerificationSource; /** Short free-form note with caveats, provenance, or recapture instructions. */ notes?: string; } /** Anti-detection configuration */ export interface AntiDetection { /** Min delay between requests in ms */ minDelayMs: number; /** Max delay between requests in ms */ maxDelayMs: number; /** Max conversations to fetch messages for per sync */ maxMessageThreads: number; /** Notes about platform-specific detection risks */ notes: string; } /** Complete platform adapter definition */ export interface PlatformAdapter { /** Platform identifier */ platform: Platform; /** Human-readable platform name */ name: string; /** Platform color for UI */ color: string; /** Platform emoji for UI */ emoji: string; // ── API Documentation ── /** How to extract auth tokens from a browser session */ auth: { /** What tokens are needed */ requiredTokens: string[]; /** How to extract them (content script instructions) */ extractionMethod: string; /** Which URLs/cookies to look for */ sources: Array<{ type: "cookie" | "meta_tag" | "local_storage" | "header" | "page_script"; name: string; selector?: string; description: string; }>; }; /** Available API endpoints */ endpoints: { /** List conversations / inbox */ listConversations: ApiEndpoint; /** Fetch messages for a specific conversation */ fetchMessages: ApiEndpoint; /** Send a message (if supported) */ sendMessage?: ApiEndpoint; /** Mark conversation as read (if supported) */ markRead?: ApiEndpoint; /** Additional platform-specific endpoints */ [key: string]: ApiEndpoint | undefined; }; /** Anti-detection settings */ antiDetection: AntiDetection; /** Deep link URL builder */ buildDeepLink: (conversation: Conversation) => string | undefined; // ── Version tracking ── /** When this adapter was last verified against the live API */ lastVerified: string; /** Known API versions / query IDs that may change */ volatileIds?: Record; /** Changelog of API changes we've observed */ changelog?: Array<{ date: string; description: string; }>; } // ── Re-exports ──────────────────────────────────────────────────────────── export { slackAdapter } from "./platforms/slack.js"; export { linkedinAdapter } from "./platforms/linkedin.js"; export { instagramAdapter } from "./platforms/instagram.js"; export { tinderAdapter } from "./platforms/tinder.js"; export { imessageAdapter } from "./platforms/imessage.js";