API types

The type definition files that define the public surface, and every package export.

The same files ship in ascent-types.zip.

types.d.ts

// Core types for the SEO module. These are the ONLY shapes the pure cores know about.
// Project-specific data reaches the cores exclusively as SeoEntity[] via the adapter,
// so no core ever references a project domain concept ("story", "author", ...).

/** One indexable page, produced by a project's adapter. */
export interface SeoEntity {
  /** Free-form project type tag, e.g. 'story' | 'author' | 'page'. */
  type: string
  /** Absolute-from-root path, e.g. '/stories/the-blue-house'. */
  url_path: string
  title: string
  /** Short excerpt used for AI context + thin-content checks. NEVER paywalled full text. */
  body_sample: string
  /** BCP-47 language tag, e.g. 'en', 'fr', 'mfe'. */
  lang: string
  /** ISO 8601 last-modified timestamp. */
  updated_at: string
  /** schema.org type to emit, e.g. 'Article' | 'Person' | 'WebPage'. */
  schema_type: string
  /** Translations of this entity, for hreflang alternates. */
  alternates?: { lang: string; url_path: string }[]
  /** Absolute image URL for OG/Twitter cards + Article schema. */
  image_url?: string
}

/** A single health-audit finding. */
export interface Issue {
  code: string
  severity: 'error' | 'warn' | 'info'
  detail: string
}

/** The doc-derived, admin-editable project profile that steers the engine. */
export interface SeoProfile {
  siteName: string
  siteUrl: string
  description: string
  geography?: string
  languages?: string[]
  audience?: string
  competitors?: string[]
  logoUrl?: string
  sameAs?: string[]
  twitterCard?: string
}

// ── v2 (autonomous research agent) ──────────────────────────────────────────────

/** One normalized search-result row from a SERP provider. */
export interface SerpResult {
  position: number
  domain: string
  url: string
  title: string
}

/** A registry source with a content hash for diff-driven re-investigation. */
export interface SourceRecord {
  id: string
  kind: string // 'project_docs' | 'own_site' | 'competitor' | 'serp' | 'search_console'
  ref: string
  content_hash?: string
}

/** Where a proposal/finding came from (attached to every proposal). */
export interface Provenance {
  source: string
  researcher: string
  competitor?: string
  rationale: string
}

/** One rank observation for a keyword. */
export interface RankRow {
  keyword: string
  our_position: number | null
  checked_at: string
}

adapter.d.ts

import type { SeoEntity } from '../core/types'

/**
 * THE consumer seam. Each project implements this one function to enumerate its
 * indexable pages as a uniform SeoEntity[]. Every core (engine, sitemap, JSON-LD,
 * health) consumes the result — so the module stays 100% project-agnostic.
 *
 * Rules:
 *  - Return only publicly-indexable, published entities.
 *  - body_sample is an EXCERPT only — never expose paywalled/full body text.
 *  - Include `alternates` for translated entities so hreflang is emitted.
 */
export function listIndexableEntities(): Promise<SeoEntity[]>

billingProvider.d.ts

// BillingProvider seam — an OPTIONAL subscription-billing abstraction for SELF-HOSTERS, mirroring the
// serpProvider / storage / notify seams. Swap adapters without touching the rest of the agent:
//   - `mips`  → hosted card payments via MIPS (billingProvider.mips.mjs). MIPS charges in the merchant's
//               configured display currency at a locked FX rate — NOT Mauritius/MUR-only.
//   - `fake`  → in-memory, for tests + local demo (billingProvider.fake.mjs).
//   - `<intl>`→ any other processor (owner-chosen). NOT assumed to be Stripe.
//
// NOTE: the hosted Ascent platform does NOT use this subscription seam — it meters paid usage with an
// account-level ops-credits WALLET (prepaid credits + top-up packs; BYOK runs on the caller's own keys).
// This seam is for self-hosters who want classic plan subscriptions. A plan name maps to entitlements via
// core/entitlements.mjs; the provider only handles money + subscription lifecycle, never feature gating.

export type SubscriptionStatus = 'active' | 'past_due' | 'canceled' | 'none'

export interface Subscription {
  plan: string
  status: SubscriptionStatus
  /** ISO 8601, or null when there's no active period. */
  renewsAt: string | null
}

export interface CheckoutSession {
  /** Where to send the customer to pay (hosted payment page / redirect). */
  redirectUrl: string
  /** Opaque reference the provider echoes back on its callback/webhook. */
  reference: string
}

export interface WebhookEvent {
  event: string
  account: string
  plan: string
  status: SubscriptionStatus
  renewsAt?: string | null
}

export interface BillingProvider {
  name: string
  /** Start a subscription: returns where to send the customer to pay. */
  createCheckout(account: string, plan: string): Promise<CheckoutSession>
  /** Current subscription for an account (→ entitlements). */
  getSubscription(account: string): Promise<Subscription>
  /** Verify + parse a provider callback/webhook. MUST throw on an invalid signature. */
  verifyWebhook(headers: Record<string, string>, rawBody: string): Promise<WebhookEvent>
  /** Cancel at period end (or immediately, provider-dependent). */
  cancel(account: string): Promise<void>
}

fetchPage.d.ts

export interface FetchPage {
  fetch(url: string): Promise<{ ok: boolean; url: string; text: string }>
}

searchConsole.d.ts

/**
 * Search Console seam (optional, off by default). Where the SERP provider reads the
 * *public* ranking of a keyword, a SearchConsole provider reads the site owner's own
 * measured query performance (clicks + impressions + average position) from Google
 * Search Console — the buyer's real traffic, the true-outcomes signal for the dashboard.
 */
export interface KeywordPosition {
  keyword: string
  position: number
  impressions: number
  clicks?: number
}

export interface GscSummary {
  clicks: number
  impressions: number
  ctr: number // percentage
  position: number
}

export interface SearchConsole {
  name: string
  positions(): Promise<KeywordPosition[]>
  summary?(): Promise<GscSummary>
}

serpProvider.d.ts

/**
 * SERP provider seam. A provider runs a search query and returns raw result
 * rows ({url, title}) — normalization (positions, domains) is serpParse's job
 * downstream. Providers are swappable and admin-configured; `google` is default.
 */
export interface SerpProviderResult {
  url: string
  title: string
}

export interface SerpSearchOpts {
  location?: string
  num?: number
}

export interface SerpProvider {
  name: string
  search(query: string, opts?: SerpSearchOpts): Promise<{ results: SerpProviderResult[] }>
}

storage.d.ts

// StorageAdapter — the storage seam (E9).
//
// Every read/write the agent performs goes through ONE object. The pure cores never
// touch storage; the backend shell injects a StorageAdapter. This makes the whole
// component storage-agnostic: back it with Supabase (the reference), Postgres/MySQL/
// SQLite elsewhere, a document/NoSQL store, or the bundled in-memory/file store for a
// zero-database deployment.
//
// SECURITY — READ THIS. The Supabase reference enforces access with Row-Level Security +
// SECURITY DEFINER RPCs. Those do NOT exist outside Postgres. When you back the agent with
// any other store, the adapter (and the surface that calls it) is solely responsible for
// authorization: only an admin context may read/write agent state; the public serving reads
// (published guides, approved page meta) must expose ONLY published/approved rows and only
// their public columns. A storage adapter with no authz is an open database.

export interface AgentSettings {
  autonomy?: Record<string, unknown>
  notifications?: Record<string, unknown>
  self_heal?: Record<string, unknown>
  budget_cap_cents_month?: number
  cost_rates?: Record<string, number>
  serp_provider?: string
  enabled_researchers?: Record<string, boolean>
  model_weights?: Record<string, unknown>
  notify_state?: Record<string, unknown>
  [k: string]: unknown
}

export interface Proposal { id: string; type: string; payload: any; score?: number | null; status: 'pending' | 'approved' | 'rejected'; rationale?: string | null }
export interface RankRow { keyword: string; our_position: number | null; top_results: Array<{ position: number; domain: string }>; checked_at: string }
export interface Job { id: string; type: string; payload: any; status: 'queued' | 'running' | 'done' | 'failed' | 'capped'; attempts: number; updated_at: string }
export interface CircuitState { consecutiveFailures: number; open: boolean }
export interface FeedbackRow { proposal_type: string; outcome: string }

export interface StorageAdapter {
  // ── settings ──
  getSettings(): Promise<AgentSettings>
  setSettings(patch: Partial<AgentSettings>): Promise<void>

  // ── budget ledger ──
  // The engine's built-in metering unit is real CENTS, capped by
  // AgentSettings.budget_cap_cents_month and enforced by core/budgetGate.mjs.
  // A host that meters in its own unit (credits, quota, seats) implements these two against ITS OWN
  // ledger and converts — monthSpend() must return cents so budgetGate can compare against the cap.
  // Returning 0 from monthSpend() with a cap of 0 disables all paid work, which is fail-safe but silent:
  // if you are substituting your own metering, gate BEFORE calling the engine rather than starving it here.
  monthSpend(): Promise<number>
  recordSpend(lineType: string, cents: number): Promise<void>

  // ── keywords ──
  getTargetKeywords(): Promise<string[]>
  upsertKeyword(k: { keyword: string; status?: string; is_target?: boolean }): Promise<void>

  // ── rankings ──
  saveRank(row: RankRow): Promise<void>
  recentRankEntries(limit?: number): Promise<Array<{ our_position: number | null; top_results: any }>>

  // ── proposals + feedback (the human/auto gate) ──
  insertProposals(rows: Array<Omit<Proposal, 'id' | 'status'>>): Promise<void>
  pendingProposals(): Promise<Proposal[]>
  pendingProposalCount(): Promise<number>
  approvedProposals(): Promise<Array<{ type: string; payload: any }>>
  applyProposal(id: string, actor: string): Promise<void> // promote pending → live + audit
  insertFeedback(rows: FeedbackRow[]): Promise<void>
  allFeedback(): Promise<FeedbackRow[]>

  // ── jobs ──
  enqueue(type: string, payload: any): Promise<void>
  claimJobs(limit: number): Promise<Job[]>
  markDone(id: string): Promise<void>
  markFailed(id: string, error: string): Promise<void>
  markCapped(id: string): Promise<void>

  // ── circuit breaker ──
  getCircuit(target: string): Promise<CircuitState>
  saveCircuit(target: string, state: CircuitState): Promise<void>
  listCircuits(): Promise<Array<{ name: string; open: boolean; openedAtIso?: string }>>

  // ── health / self-checks ──
  saveHealthChecks(results: Array<{ name: string; ok: boolean; detail?: string }>): Promise<void>
  recentHealthAllOk(): Promise<{ ok: boolean; failed: string[] }>

  // ── notifications (E2) ──
  notify(event: any, cfg: any): Promise<void>
  getNotifyState(): Promise<Record<string, any>>
  saveNotifyState(state: Record<string, any>): Promise<void>

  // ── model (E6) ──
  saveModel(model: any): Promise<void>

  // NOTE: content (E3), self-heal revert (E4), competitor-matrix (E8) methods extend this
  // same object in the reference Supabase adapter. A minimal store may omit them; the agent
  // degrades gracefully (those features simply produce nothing).
}

webSearch.d.ts

export interface WebSearchResult {
  url: string
  title: string
  snippet?: string
}

export interface WebSearch {
  name: string
  search(query: string, opts?: { num?: number }): Promise<{ results: WebSearchResult[] }>
}

Package exports

@ascent/seo-agent version 1.0.0. Not on a public registry; licensed customers receive it from Umbodi. The MCP server ships as seo-agent-mcp.

Import pathExports
@ascent/seo-agentcreateLicenseGate createSeoAgent memoryStore resolveConfig resolveModality resolveRuntime resolveViewport sitemapAdapter verifyLicense
@ascent/seo-agent/licensecreateLicenseGate verifyLicense
@ascent/seo-agent/configPROFILES resolveConfig
@ascent/seo-agent/runtimeresolveRuntime
@ascent/seo-agent/coresPLAN_NAMES articleSchema auditEntities breadcrumbSchema entitiesFromSitemap faqSchema joinOrigin mergeMeta normalizeLastmod organizationSchema parseSitemap pathFromUrl personSchema pickSitemapUrl planEntitlements renderJsonLd requireEntitlement slugify titleFromPath websiteSchema
@ascent/seo-agent/servicecreateSeoService defaultHandlers resolveRequestDeps toNodeHandler
@ascent/seo-agent/mcpconfigSnippet defineTools handleMessage localBackend remoteBackend resolveBackend runServer
@ascent/seo-agent/widgetsattrs buildHeadTags buildHealthGauge buildRankBadge buildResultCard esc formFactor injectHead isTouch makeAscentClient mount mountHealthGauge mountRankBadge mountResultCard
@ascent/seo-agent/adminactionButton buildAutonomyDial buildCompetitors buildKeywords buildProposals buildRankings buildStatusPanel handleActionClick mountAutonomyDial mountCompetitors mountKeywords mountPanel mountProposals mountRankings mountStatusPanel table