> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livepeer.org/llms.txt
> Use this file to discover all available pages before exploring further.

# NaaP Changelog

> Commit history for NaaP (Network as a Platform), the plugin platform for the Livepeer AI Compute Network.

export const CustomCardTitle = ({icon, title, variant = "card", iconSize, style = {}, className = "", ...rest}) => {
  const variants = {
    card: {
      display: 'flex',
      alignItems: 'center',
      gap: "var(--lp-spacing-2)",
      marginBottom: "var(--lp-spacing-3)",
      color: 'var(--lp-color-text-primary)',
      fontSize: '1rem',
      fontWeight: 600
    },
    accordion: {
      display: 'inline-flex',
      alignItems: 'center',
      gap: "var(--lp-spacing-2)"
    },
    tab: {
      display: 'inline-flex',
      alignItems: 'center',
      gap: '0.4rem',
      fontSize: '0.875rem'
    }
  };
  const sizes = {
    card: 20,
    accordion: 18,
    tab: 14
  };
  const size = iconSize || sizes[variant] || 20;
  const baseStyle = variants[variant] || variants.card;
  return variant === 'card' ? <div className={className} style={{
    ...baseStyle,
    ...style
  }} {...rest}>
      {typeof icon === 'string' ? <Icon icon={icon} size={size} color="var(--lp-color-accent)" /> : icon}
      {title}
    </div> : <span className={className} style={{
    ...baseStyle,
    ...style
  }} {...rest}>
      {typeof icon === 'string' ? <Icon icon={icon} size={size} color="var(--lp-color-accent)" /> : icon}
      {title}
    </span>;
};

export const Subtitle = ({style = {}, text, children, variant = 'default', fontSize = '', fontWeight = '', fontStyle = '', marginTop = '', marginBottom = '', color = '', className = '', ...rest}) => {
  const renderInlineCode = (value, keyPrefix) => {
    return value.split(/(`[^`]+`)/g).map((segment, index) => {
      if (segment.startsWith('`') && segment.endsWith('`')) {
        return <code key={`${keyPrefix}-code-${index}`}>{segment.slice(1, -1)}</code>;
      }
      return segment;
    });
  };
  const renderInlineMarkup = (value, keyPrefix = 'subtitle') => {
    if (typeof value !== 'string') {
      return value;
    }
    return value.split(/(\*\*[\s\S]+?\*\*)/g).map((segment, index) => {
      if (segment.startsWith('**') && segment.endsWith('**')) {
        const inner = segment.slice(2, -2);
        return <strong key={`${keyPrefix}-strong-${index}`}>
            {renderInlineCode(inner, `${keyPrefix}-strong-${index}`)}
          </strong>;
      }
      return renderInlineCode(segment, `${keyPrefix}-${index}`);
    });
  };
  const renderContent = (value, keyPrefix) => {
    if (Array.isArray(value)) {
      return value.map((item, index) => renderContent(item, `${keyPrefix}-${index}`));
    }
    return renderInlineMarkup(value, keyPrefix);
  };
  const variants = {
    default: {
      fontSize: '1rem',
      fontStyle: 'italic',
      color: 'var(--lp-color-accent)',
      marginBottom: 0
    },
    changelog: {
      fontSize: '0.8rem',
      fontStyle: 'normal',
      fontWeight: 700,
      color: 'var(--lp-color-text-primary)',
      marginBottom: 0
    }
  };
  const base = variants[variant] || variants.default;
  return <span className={className} style={{
    ...base,
    ...fontSize ? {
      fontSize
    } : {},
    ...fontWeight ? {
      fontWeight
    } : {},
    ...fontStyle ? {
      fontStyle
    } : {},
    ...marginTop ? {
      marginTop
    } : {},
    ...marginBottom ? {
      marginBottom
    } : {},
    ...color ? {
      color
    } : {},
    ...style
  }} {...rest}>
      {renderContent(text, 'text')}
      {renderContent(children, 'children')}
    </span>;
};

export const LazyLoad = ({children, height = "200px", offset = "200px", fadeDuration = 400, className = "", style = {}, ...rest}) => {
  const ref = useRef(null);
  const [visible, setVisible] = useState(false);
  const [ready, setReady] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setVisible(true);
        observer.disconnect();
      }
    }, {
      rootMargin: offset
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, []);
  useEffect(() => {
    if (!visible) return;
    const frameId = requestAnimationFrame(() => {
      setReady(true);
    });
    return () => cancelAnimationFrame(frameId);
  }, [visible]);
  const placeholder = <div ref={ref} className={className} style={{
    minHeight: height,
    ...style
  }} {...rest} />;
  if (!visible) return placeholder;
  return <div ref={ref} className={className} style={{
    opacity: ready ? 1 : 0,
    transition: `opacity ${fadeDuration}ms ease-in`,
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const ScrollBox = ({children, maxHeight = 300, showHint = true, ariaLabel = "Scrollable content", style = {}, className = "", ...rest}) => {
  const contentRef = useRef(null);
  const [isOverflowing, setIsOverflowing] = useState(false);
  useEffect(() => {
    const checkOverflow = () => {
      if (contentRef.current) {
        const maxHeightPx = typeof maxHeight === "number" ? maxHeight : parseInt(maxHeight, 10) || 300;
        setIsOverflowing(contentRef.current.scrollHeight > maxHeightPx);
      }
    };
    checkOverflow();
    window.addEventListener("resize", checkOverflow);
    return () => window.removeEventListener("resize", checkOverflow);
  }, [maxHeight, children]);
  return <div className={className} style={{
    position: "relative",
    ...style
  }} {...rest}>
      <div ref={contentRef} role="region" tabIndex={0} aria-label={ariaLabel} style={{
    maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
    overflowY: "auto",
    paddingRight: 4
  }} onScroll={e => {
    const el = e.target;
    const atBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 10;
    const hint = el.parentNode.querySelector("[data-scroll-hint]");
    if (hint) hint.style.opacity = atBottom ? "0" : "1";
  }}>
        {children}
      </div>
      {showHint && isOverflowing && <div data-scroll-hint style={{
    fontSize: 11,
    color: "var(--lp-color-text-muted)",
    textAlign: "center",
    marginTop: 8,
    transition: "opacity 0.2s"
  }}>
          Scroll for more ↓
        </div>}
    </div>;
};

export const DoubleIconLink = ({label = '', labelColor, href = '#', text = '', iconLeft = 'github', iconLeftColor, iconRight = 'arrow-up-right', iconRightColor = 'var(--lp-color-accent)', className = '', style = {}, ...rest}) => {
  return <span className={className} style={{
    whiteSpace: 'nowrap',
    display: 'inline-flex',
    alignItems: 'center',
    gap: "var(--lp-spacing-1)",
    marginLeft: '0.3rem',
    ...style
  }} {...rest}>
      {text && <span style={{
    marginRight: 8
  }}>{text}</span>}
      <Icon icon={iconLeft} color={iconLeftColor} />
      <a href={href} style={{
    color: {
      labelColor
    }
  }}>
        {label}
      </a>
      <div style={{
    marginRight: '0.3rem'
  }}>
        <Icon icon={iconRight} size={12} color={iconRightColor} />
      </div>
    </span>;
};

export const LinkArrow = ({href, label, description, newline = true, borderColor, className = '', style = {}, ...rest}) => {
  const linkArrowStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: "var(--lp-spacing-1)",
    width: 'fit-content',
    ...borderColor && ({
      borderColor
    })
  };
  return <span className={className} style={style} {...rest}>
      {newline && <br />}
      <span style={linkArrowStyle}>
        <a href={href} target="_blank" rel="noopener noreferrer">
          {label}
        </a>
        <Icon icon="arrow-up-right" size={14} color="var(--lp-color-accent)" />
      </span>
      {description && description}
      {description && <div style={{
    height: "var(--lp-spacing-3)"
  }} />}
    </span>;
};

export const InlineDivider = ({margin = "0.75rem 0", padding = "0", color = "var(--lp-color-border-default)", opacity = 0.4, height = "1px", className = "", style = {}, ...rest}) => <hr role="separator" className={className} style={{
  border: "none",
  margin,
  padding,
  height,
  backgroundColor: color,
  opacity,
  ...style
}} {...rest} />;

export const CustomDivider = ({color = "var(--lp-color-border-default)", middleText = "", spacing = "default", style = {}, className = "", ...rest}) => {
  const spacingPresets = {
    default: {
      margin: "24px 0"
    },
    overlap: {
      margin: "-1rem 0 -1rem 0"
    },
    tight: {
      margin: "0 0 -1rem 0"
    },
    section: {
      margin: "0 0 -2rem 0"
    },
    sectionOverlap: {
      margin: "-1rem 0 -2rem 0"
    },
    deepOverlap: {
      margin: "-1rem 0 -1.5rem 0"
    }
  };
  const spacingStyle = spacingPresets[spacing] || spacingPresets.default;
  return <div role="separator" aria-orientation="horizontal" className={className} style={{
    display: "flex",
    alignItems: "center",
    ...spacingStyle,
    fontSize: style?.fontSize || "16px",
    height: "fit-content",
    ...style
  }} {...rest}>
      <span style={{
    marginRight: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
      </span>
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      {middleText && <>
          <Icon icon="circle" size={2} />
          <span style={{
    margin: "0 8px",
    fontWeight: "bold",
    color: color,
    opacity: 0.7
  }}>
            {middleText}
          </span>
          <Icon icon="circle" size={2} />
        </>}
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      <span style={{
    marginLeft: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <span style={{
    display: "inline-block",
    transform: "scaleX(-1)"
  }}>
          <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
        </span>
      </span>
    </div>;
};

export const owner_0 = undefined

export const name_0 = undefined

<Tip>
  This page is an automated workflow.
  <Subtitle variant="changelog" style={{fontSize: "0.95rem", marginTop: "0.25rem"}}>Subscribe to this changelog's <LinkArrow label="RSS Feed" href="/v2/resources/changelog/naap/rss.xml" newline={false} /></Subtitle>
</Tip>

<CustomDivider style={{margin: "-0.5rem 0 -1.5rem 0"}} />

Track changes to <LinkArrow label="NaaP" href="https://github.com/livepeer/naap" newline={false} /> on GitHub.

<CustomDivider />

<Update label="feat(gateway): add ClickHouse HTTP query connector with s..." tags={["Commit"]} rss={{ title: "NaaP: feat(gateway): add ClickHouse HTTP query connector with s...", description: "feat(gateway): add ClickHouse HTTP query connector with static query support (#212)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## feat(gateway): add ClickHouse HTTP query connector with s...

  <ScrollBox maxHeight="150px" showHint={false}>
    feat(gateway): add ClickHouse HTTP query connector with static query support (#212)

    * feat(gateway): add ClickHouse HTTP query connector with static query support
      Wire `upstreamStaticBody` through the connector template pipeline (loader
      interface, JSON schema, admin template route, seed script) so endpoints
      can send a pre-configured request body to the upstream service.
      Add the `clickhouse-query` connector template with four endpoints:
      * /network\_prices  – static SQL for orchestrator pricing data
      * /query           – dynamic SELECT-only queries with regex + blacklist
      * /ping            – ClickHouse health check
      * /tables          – list tables via SHOW TABLES
        Include a how-to guide with dashboard visualisation example and update
        the connector catalogue.
        Made-with: Cursor
    * fix(gateway): prevent consumer Authorisation header from being forwarded with basic auth
      Updated the buildUpstreamRequest and buildUpstreamHeaders functions to ensure that when the connector's authType is set to 'basic', the consumer's Authorisation header is not forwarded to the upstream service. This change enhances security by preventing potential credential leaks. Added corresponding tests to verify this behaviour.
    * fix: address PR 212 code review feedback

    - Use ?? null instead of || null for upstreamStaticBody to preserve
      empty strings and match the seed script's nullish coalescing
    - Remove hardcoded allowedHosts from clickhouse-query.json so host
      validation derives from upstreamBaseUrl at runtime, avoiding
      conflicts when users override the base URL
    - Remove envKey from clickhouse-query.json – a single env var cannot
      map to two separate Basic-auth secrets (username + password);
      users configure credentials via the Settings tab UI instead
    - Add JSDoc docstrings to all exported interfaces and functions in
      the connector template loader and route handlers
      Made-with: Cursor

    * fix: address remaining PR 212 review nitpicks

    - Add conditional JSON Schema validation requiring upstreamStaticBody
      when bodyTransform is "static"
    - Remove empty Request init object in basic-auth test
    - Remove stale gw\_not\_forwarded assertion that always passed trivially
      Made-with: Cursor

    * fix: require bodyTransform presence in conditional schema validation
      Without "required": \["bodyTransform"] in the if-block, endpoints that
      omit bodyTransform (defaulting to passthrough) would incorrectly pass
      the condition and be forced to provide upstreamStaticBody.
      Made-with: Cursor

    ***
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/1c097832d4f4a7a4a01e9dd3e861f85ac1c4b9b7" iconLeft="github" />
</Update>

<Update label="fix: migrate docker-compose v1 to docker compose v2 (#207)" tags={["Commit"]} rss={{ title: "NaaP: fix: migrate docker-compose v1 to docker compose v2 (#207)", description: "fix: migrate docker-compose v1 to docker compose v2 (#207)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## fix: migrate docker-compose v1 to docker compose v2 (#207)

  <ScrollBox maxHeight="150px" showHint={false}>
    fix: migrate docker-compose v1 to docker compose v2 (#207)

    * fix: migrate docker-compose v1 commands to docker compose v2
      docker-compose (v1) is deprecated and not installed by default on modern
      Docker setups. Updates all active scripts and docs to use `docker compose`
      (v2 plugin), with a v1 fallback in shell scripts for backwards compat.
    * fix: set DATABASE\_URL\_UNPOOLED for prisma db push in all scripts (#210)
      The Prisma schema requires both DATABASE\_URL and DATABASE\_URL\_UNPOOLED
      (directUrl for non-pooled connections used during migrations). The db
      push commands in start.sh, db-setup.sh, and setup-db-simple.sh only
      set DATABASE\_URL, causing silent schema push failures and empty tables
      on first run.

    ***
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/74282977defcc758c0ef014f8539204787447f91" iconLeft="github" />
</Update>

<Update label="fix: remove deployment-manager plugin accidentally commit..." tags={["Commit"]} rss={{ title: "NaaP: fix: remove deployment-manager plugin accidentally commit...", description: "fix: remove deployment-manager plugin accidentally committed to main (#217)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## fix: remove deployment-manager plugin accidentally commit...

  <ScrollBox maxHeight="150px" showHint={false}>
    fix: remove deployment-manager plugin accidentally committed to main (#217)
    The deployment-manager plugin was inadvertently included in PR #152
    (feat(service-gateway): admin APIs + policy engine). This commit removes:

    * plugins/deployment-manager/ (54 files: frontend, backend, plugin.json)
    * /deployments route from middleware PLUGIN\_ROUTE\_MAP
    * .github/workflows/examples/cd-deploy-serverless.yml (CD workflow)
    * deployment-manager references from docs/plugin-port-howto.md
      The existing stale-plugin cleanup in bin/sync-plugin-registry.ts will
      automatically soft-disable the WorkflowPlugin DB row on next deploy.
      The plugin will be re-introduced when feat/deployment\_manager opens a PR
      and is merged to main.
      Made-with: Cursor
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/ae132cce42d8aa82649dc3868644e700e7d2232d" iconLeft="github" />
</Update>

<Update label="chore: sync lockfile and fix CI Vercel build (Corepack + ..." tags={["Commit"]} rss={{ title: "NaaP: chore: sync lockfile and fix CI Vercel build (Corepack + ...", description: "chore: sync lockfile and fix CI Vercel build (Corepack + DB guard) (#215)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## chore: sync lockfile and fix CI Vercel build (Corepack + ...

  <ScrollBox maxHeight="150px" showHint={false}>
    chore: sync lockfile and fix CI Vercel build (Corepack + DB guard) (#215)
    Regenerate package-lock.json so it matches package.json after #201
    (optional platform-specific dev deps: @napi-rs/wasm-runtime, @nx/nx-*,
    @oxc-resolver/binding-*).
    Update bin/vercel-build.sh for CI and local parity with Vercel:

    * Enable Corepack before the Next.js build so packageManager (npm\@10.9.4)
      is honoured and preinstalled Yarn 1.x does not break next build.
    * Run sync-plugin-registry only when VERCEL\_ENV is production or preview,
      matching the prisma db push guard; CI uses DATABASE\_URL without a
      running Postgres.
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/4d1853d9dd9cc3a2b1c2aa746597e15627edbc59" iconLeft="github" />
</Update>

<Update label="feat(gateway): Agent Tool Interface — discovery, pricing,..." tags={["Commit"]} rss={{ title: "NaaP: feat(gateway): Agent Tool Interface — discovery, pricing,...", description: "feat(gateway): Agent Tool Interface — discovery, pricing, MCP, metrics, and master keys (#201)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## feat(gateway): Agent Tool Interface – discovery, pricing,...

  <ScrollBox maxHeight="150px" showHint={false}>
    feat(gateway): Agent Tool Interface – discovery, pricing, MCP, metrics, and master keys (#201)

    * feat(gateway): add agent tool interface with discovery, pricing, MCP, and metrics
      Implement the complete agent tool layer for the Service Gateway, enabling
      AI agent frameworks (MCP, OpenAI, LangChain) to discover, price, and
      invoke tools through standardised protocols.
      Schema:

    - ConnectorPricing, GatewayMasterKey, ConnectorMetrics, ConnectorCapabilityRanking
    - Agent metadata fields on ServiceConnector (agentDescription, agentNotFor, inputSchema, outputSchema)
    - Examples field on ConnectorEndpoint
      Auth:
    - Master key (gwm\_ prefix) auth path in authorise.ts
    - Rate-limited, SHA-256 hashed, supports team + personal scope
      APIs (public):
    - GET /gw/catalogue (native, OpenAI, MCP formats)
    - GET /gw/pricing, /gw/pricing/:slug/estimate
    - POST /gw/mcp (JSON-RPC 2.0 tools/list + tools/call)
    - GET /gw/discovery (no auth, standard entry point)
    - GET /gw/rankings?category=... (cross-connector rankings)
    - GET /gw/catalogue/:slug/metrics, /gw/catalogue/:slug/rankings
      APIs (admin):
    - Master key CRUD (create, list, revoke, rotate)
    - Connector pricing CRUD
    - Connector rankings CRUD
    - Metrics aggregation cron job (hourly)
      Reliability:
    - Idempotency-Key support in proxy route via @naap/cache
    - Agent-friendly error recovery (retryable, suggestedAction, Retry-After)
    - Reserved slug validation to prevent route collisions
      Frontend:
    - MasterKeysPage (create, revoke, rotate gwm\_ keys)
    - Pricing tab on ConnectorDetailPage
    - Agent Metadata section in Settings tab
    - Performance tab with latency distribution, error rate, availability
      Config:
    - Idempotency-Key added to CORS headers (vercel.json + next.config.js)
    - .well-known/gateway.json rewrite to /gw/discovery
    - Metrics aggregation cron (5 \* \* \* \*)
      Tests: 35 files, 354 tests (baseline: 29 files, 294 tests), 0 regressions
      Made-with: Cursor

    * fix(gateway): address code review findings – security, correctness, and error handling

    - Fix CRITICAL auth bypass in metrics aggregate endpoint when CRON\_SECRET is unset
    - Fix SSRF vulnerability in MCP route by using env-based self-origin instead of Host header
    - Fix MCP tools/call silently dropping args for GET endpoints – now converts to query params
    - Fix pricing logic where volume tiers incorrectly overrode feature-specific pricing
    - Fix PerformanceTab bypassing auth by using raw fetch – now uses api hook
    - Fix variable shadowing of global `window` in PerformanceTab (renamed to timeWindow)
    - Fix AgentMetadata silently returning on invalid JSON – now shows error message
    - Fix MasterKeysPage missing error handling on create/revoke/rotate
    - Fix rankings batch upsert lacking transaction – now uses \$transaction
    - Fix discovery document missing rankings endpoint
    - Fix NaN pagination and add pageSize cap (max 200) in catalogue route
    - Fix mcp-adapter discarding agentDescription content in tool descriptions
    - Fix calculateCost returning empty connector field – now accepts connectorSlug param
    - Add createdAt to ConnectorPricing and ConnectorCapabilityRanking models
    - Add missing ownerUserId index to GatewayMasterKey
    - Prevent creating master keys with zero scopes
    - Add tests for feature-pricing precedence and connector slug passthrough
      Made-with: Cursor

    * fix(gateway): address CodeQL SSRF finding in MCP route
      Use catalogue-validated connector slug instead of user-provided value
      in the self-fetch URL. Apply encodeURIComponent to prevent path
      traversal. This ensures the URL path only references connectors
      that exist in the published catalogue.
      Made-with: Cursor
    * fix(gateway): fix template navigation, edit flow, single-select, default filter, and archive actions

    - Fix template click from empty state navigating to template list instead of selected template
    - Fix draft connector edit opening template step instead of connect step with pre-filled data
    - Enforce single template selection (radio) instead of multi-select (checkbox)
    - Default connector listing to show published connectors instead of all statuses
    - Add recover-to-draft and permanent purge actions for archived connectors
    - Add connector.purge audit action and backend purge endpoint with ?purge=true
    - Add status field to updateConnectorSchema for recover workflow
      Made-with: Cursor

    * fix(gateway): address PR #201 review comments from Copilot and CodeRabbit
      Security:

    - Add scope/visibility filtering to catalogue, pricing, metrics, and rankings
      endpoints to prevent leaking private/team connectors
    - Wrap MCP auth/internal errors in JSON-RPC 2.0 envelope
    - Forward x-team-id, x-request-id, x-trace-id headers in MCP tools/call
      Correctness:
    - Fix metrics period mismatch: change catalogue/metrics queries from 'daily' to
      'hourly' and add daily rollup in aggregation cron
    - Fix percentile calculation using ceil-based nearest-rank formula
    - Set 7d percentiles to null (not composable via weighted average)
    - Fix inputSchema precedence: endpoint bodySchema now takes priority over
      connector inputSchema
    - Include HTTP method in idempotency cache key to prevent cross-method collisions
    - Use arrayBuffer+base64 for idempotency storage to avoid binary corruption
    - Await cacheGet in getIdempotentResponse so rejections are caught
      Improvements:
    - Fix Performance tab using wrong API client (admin vs public catalogue)
    - Add copy feedback and revoke confirmation to MasterKeysPage
    - Update z.record() calls for Zod v4 forward compatibility
    - Consolidate duplicate requestId/traceId extraction in pricing routes
    - Remove redundant Content-Type header in discovery route
    - Remove redundant @@index from ConnectorMetrics schema
    - Add take:100 cap to rankings endpoint
    - Rename misleading test description
      Made-with: Cursor

    * feat(gateway): add sub-navigation and promote Agent tab

    - Add GatewayNav horizontal sub-navigation (Connectors, Dashboard, Master Keys, Plans)
    - Render GatewayNav above Routes inside MemoryRouter for persistent navigation
    - Promote AgentMetadataSection from embedded in Settings to independent Agent tab
    - Add ARIA accessibility attributes (role=tablist, role=tab, aria-selected)
    - Add overflow-x-auto and whitespace-nowrap for tab bar on narrow viewports
    - Add loading state to Agent tab instead of rendering nothing
    - Guard against undefined res.data in AgentMetadataSection
    - Update file header to reflect current 9-tab layout
      Made-with: Cursor

    * fix(gateway): switch to side nav, fix dashboard infinite loop, improve UX

    - Replace horizontal tab nav with compact left sidebar (208px) featuring
      plugin title, subtitle, and icon-labeled nav items
    - Fix DashboardPage infinite re-render loop caused by Date.now() in
      useCallback dependency array (moved computation inside callback)
    - Simplify ConnectorListPage header to avoid title duplication with sidebar
    - Layout changed from flex-col to flex for side-by-side nav + content
      Made-with: Cursor

    * fix(gateway): address remaining PR review comments

    - Add scope/visibility filtering to catalogue/\[slug], pricing list,
      and pricing estimate routes (OR public/teamId)
    - Enforce master key scopes: return scopes and allowedIPs from
      authorizeMasterKey, check 'proxy' scope on the engine route
    - Add masterKeyScopes field to AuthResult type
    - Decouple rawKey delivery from audit log success in master key
      creation (fire-and-forget audit)
    - Eliminate N+1 queries in metrics aggregation cron by batch-fetching
      all usage records and health checks upfront
    - Change catalogue/\[slug] metrics query from 'daily' to 'hourly' period
      Made-with: Cursor

    * feat(gateway): add plan selector to API key creation

    - Add plan dropdown to API Keys tab creation form
    - Fetch available plans when API Keys tab is active
    - Pass planId to POST /keys when a plan is selected
    - Display plan name in the keys table (new Plan column)
    - Add plan field to ApiKey interface
      Made-with: Cursor

    * feat(gateway): add configurable default plan for planless API keys
      Every scope now has an auto-created "default" plan (100 req/min,
      10K daily, 100K monthly) that applies to API keys with no explicit
      plan. The default plan is fully editable but cannot be deleted.

    - Add getOrCreateDefaultPlan utility with upsert for race safety
    - Integrate into authorise.ts so planless API keys inherit limits
    - Auto-create default plan on Plans list GET
    - Prevent deletion of the default plan in DELETE handler
    - Fix PUT handler to work with personal scopes (not just teamId)
    - Update PlansPage: default badge, inline editing, sort default first
    - Show "Default" label for planless keys in ConnectorDetailPage
      Made-with: Cursor

    * fix(gateway): fix Play tab 401 by sending proper auth headers
      The Play tab was sending API keys via x-api-key header, but the
      gateway proxy expects Authorisation: Bearer. Also adds
      credentials: 'include' so session cookies are sent for JWT auth,
      and x-team-id header for scope resolution. PerformanceTab fetch
      also updated with credentials and team header.
      Made-with: Cursor
    * feat(lightning-client): rewrite plugin to match Livepeer AI Video reference client
      Replace the old ffmpeg/trickle-based media bridge with WebSocket JPEG
      frame streaming that matches the upstream Livepeer lightweight gateway
      protocol.
      Frontend: new App.tsx with camera capture, dual canvas (input/output),
      real-time stats, and log panel. HTTP calls (start-job, stop-job) route
      through the livepeer-gateway Service Gateway connector. WebSocket
      connects directly to the backend proxy for frame exchange.
      Backend: rewritten as a simple Express + ws server that proxies
      bidirectional WebSocket connections between the browser and the
      upstream gateway's /ws/stream endpoint. Removed ffmpeg, trickle,
      and node-fetch dependencies.
      Added plugin.json manifest, vite.config.ts, and mount.tsx scaffolding.
      Removed obsolete VideoPlayer.tsx and useMediaBridge.ts.
      Made-with: Cursor
    * Add Replicate and RunPod providers with training support

    - replicate.py: Full inference + training, uses model-specific
      endpoint (/v1/models/{owner_0}/{name_0}/predictions) for named models
    - runpod.py: Full inference + training via serverless API
    - Both support train, train\_submit, train\_status methods
    - Adapter: multi-capability registration, runtime cap add/remove API
    - Synced all container code with deployed VM versions

    * feat: standalone demo client routes HTTP via NaaP connector, WS direct to upstream

    - Replace direct gateway calls with NaaP Service Gateway connector routing
    - Add CORS support (Access-Control-Allow-Origin: \*) for gateway consumer routes
    - Add OPTIONS preflight handler to gateway engine route
    - Demo page fields: NaaP URL, connector slug, API key, upstream WS URL
      Made-with: Cursor

    * chore: default demo page NaaP URL to localhost:3000 for local dev
      Made-with: Cursor
    * fix: address PR 201 review feedback – security, idempotency, metrics, MCP
      Security:

    - Enforce master key IP allowlist in authorizeMasterKey using matchIPAllowlist
    - Replace regex-based IP/CIDR validation with net.isIP() + prefix-length checks
      Idempotency:
    - Store content-type in cached responses for correct replay
    - Preserve content-type header when replaying idempotent responses
      Metrics:
    - Fix percentile calculation (Math.floor zero-based nearest-rank)
    - Add daily rollup aggregation from hourly metrics
    - Fix 24h window to query hourly period with proper aggregation
    - Batch connector processing with Promise.allSettled (concurrency=5)
      MCP:
    - Harden SSRF: use NEXT\_PUBLIC\_APP\_URL instead of NEXTAUTH\_URL/PORT
    - Use JSON-RPC -32000 error code for auth failures
      Config:
    - Add /.well-known/gateway.json rewrite for standard discovery location
    - Add specific function timeout (120s) for metrics aggregate cron route
      Made-with: Cursor

    ***
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/b5284afcc9f1c400ff739ee745c9ac5c590eddd7" iconLeft="github" />
</Update>

<Update label="feat: add plugin development skill guide for agent-driven..." tags={["Commit"]} rss={{ title: "NaaP: feat: add plugin development skill guide for agent-driven...", description: "feat: add plugin development skill guide for agent-driven plugin creation (#204)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## feat: add plugin development skill guide for agent-driven...

  <ScrollBox maxHeight="150px" showHint={false}>
    feat: add plugin development skill guide for agent-driven plugin creation (#204)

    * feat: add plugin development skill guide for agent-driven plugin creation
      Comprehensive agent skill document that codifies the NaaP plugin
      architecture conventions -- scaffold structure, manifest rules, frontend/
      backend patterns, UX compliance checklist, exception file handling
      (Prisma schema, API routes, middleware), testing, and clean removal.
      Cross-verified against hello-world, service-gateway, and community plugins.
      Made-with: Cursor
    * docs: add plugin port assignment reference
      Documents how plugins declare and resolve ports today – plugin.json
      manifest fields, bin/start.sh orchestration, PORT env var override
      chain, and the current port matrix across all plugins.
      Made-with: Cursor
    * docs: add Vercel production routing section to port howto
      Documents the hybrid deployment model – plugin backends run as Next.js
      Serverless Functions (no ports), the catch-all proxy with env-var
      overrides, the 501 Vercel guard, base-svc proxy, frontend CDN asset
      serving, and vercel.json function limits.
      Made-with: Cursor
    * feat: add local dev workflow and Vercel deployment sections to plugin skill
      Add comprehensive coverage for the two deployment modes:

    - Section 10 (Local Development): start.sh commands, plugin loading flow,
      backend proxying via catch-all route, env vars, build-plugins.sh usage,
      and a local dev checklist for new plugins.
    - Section 11 (Vercel Production Deployment): vercel-build.sh pipeline,
      critical constraint that Express backends do not run on Vercel (catch-all
      returns 501 for localhost), vercel.json config, CDN bundle serving,
      env var requirements, CI/CD pipeline, and Vercel deployment checklist.
    - Section 12 (Production Readiness): expanded with Local Dev and Vercel
      Deployment sub-checklists to catch deployment-breaking issues before merge.
      Made-with: Cursor

    * fix: address CodeRabbit review comments

    - Add devEntry field to PluginFrontend and PluginBackend interfaces
      to align type definitions with actual plugin.json usage
    - Add isolation field to plugin.json example in skills guide and
      document all three modes (none, iframe, worker)
    - Scope port resolution claim: clarify which backends follow the
      plugin.json fallback chain vs direct env+hardcoded resolution
    - Document the devPort 3020 collision between dashboard-data-provider
      and hello-world as a known legacy conflict
    - Clarify ports.ts as legacy/optional; plugin.json files are the
      primary source of truth for port allocation
    - Mark deprecated plugins in port matrix and env-var override table
      Made-with: Cursor

    * docs: add missing language specifiers and update port allocation appendix
      Made-with: Cursor
    * fix: replace invalid 4xxx placeholder with valid JS in Express example
      Made-with: Cursor

    ***
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/2accd6701f24cd24317bfd019bbe59c7298adeee" iconLeft="github" />
</Update>

<Update label="chore: remove deprecated plugins (gateway-manager, orches..." tags={["Commit"]} rss={{ title: "NaaP: chore: remove deprecated plugins (gateway-manager, orches...", description: "chore: remove deprecated plugins (gateway-manager, orchestrator-manager, network-analytics, hello-world, todo-list) (#205)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## chore: remove deprecated plugins (gateway-manager, orches...

  <ScrollBox maxHeight="150px" showHint={false}>
    chore: remove deprecated plugins (gateway-manager, orchestrator-manager, network-analytics, hello-world, todo-list) (#205)

    * chore: remove deprecated plugins (gateway-manager, orchestrator-manager, network-analytics, hello-world, todo-list)
      Clean removal of 5 unused plugins across the entire codebase:
      Deleted:

    - 5 example plugin directories (examples/)
    - 3 standalone backend services (services/workflows/)
    - 3 standalone web apps (apps/workflows/)
    - 2 Next.js API route directories
    - 1 MDX doc page (hello-world.mdx)
      Updated:
    - Port configs (ports.ts x2, portAllocator.ts, config/index.ts)
    - API proxy route (PLUGIN\_ENV\_MAP, SHORT\_ALIASES)
    - Middleware route map (PLUGIN\_ROUTE\_MAP)
    - API client (removed gatewayApi, orchestratorApi, analyticsApi)
    - Shell scripts (start.sh, health-check.sh, db-seed/migrate/reset.sh)
    - UMD migration script (PLUGIN\_CONFIGS)
    - .gitignore, CODEOWNERS
    - Environment files (.env.example, .env.local.example)
    - \~22 documentation and MDX content files
    - Test files (cdn-serve.test.ts, integration.test.ts)
    - Marketplace UI mock data (2 files)
    - JSDoc examples in plugin-sdk components
    - init-schemas.sql comment
    - cleanup-moved-plugins.ts filter lists
    - package-lock.json (regenerated, stale entries removed)
      Verified: Next.js production build passes, no regressions,
      zero linter errors, all modified tests pass (50/50).
      Made-with: Cursor

    * fix: regenerate lockfile preserving cross-platform rollup binaries
      The previous lockfile was regenerated on macOS which dropped the
      node\_modules/@rollup/rollup-linux-x64-gnu resolved entry. CI on
      linux-x64 runners could not find the native module, failing Shell
      Tests, SDK Tests, Lifecycle BDD, and Build jobs. Also remove stale
      workspace entries for the deleted plugin directories.
      Made-with: Cursor
    * fix: remove broken symlinks to deleted example plugins
      plugins/gateway-manager, plugins/orchestrator-manager, and
      plugins/network-analytics were symlinks to ../examples/\* directories
      that were deleted in the cleanup commit. The broken symlinks caused
      GitHub Actions hashFiles('plugins/\*\*') to fail in the Build job.
      Made-with: Cursor
    * refactor: update plugin port configurations and enhance health check script

    - Updated .env.example to reflect new canonical ports for plugins.
    - Modified health-check.sh to align service URLs with updated port configurations.
    - Introduced a new pluginPorts.ts file to centralise port management and provide utility functions for extracting and resolving plugin ports.
    - Added comprehensive tests for the new port management functionality to ensure correctness and reliability.
      These changes improve the maintainability of the port configurations and enhance the overall robustness of the service health checks.

    * fix: align port configuration with canonical PLUGIN\_PORTS mapping

    - Delete scripts/init-schemas.sql (redundant with docker/init-schemas.sql,
      used bare schema names instead of canonical plugin\_\* prefixes)
    - Fix portAllocator.ts RESERVED\_PORTS: correct labels, remove orphaned
      port 4002, add missing 4003 (capacity-planner) and 4111 (daydream-video)
    - Fix bin/health-check.sh: update all default ports to match PLUGIN\_PORTS
    - Fix docs/runbook.md: correct Daydream Video port (4010→4111), add
      Plugin Publisher entry
    - Fix .env.example: align WALLET, DASHBOARD, DAYDREAM\_VIDEO, DEVELOPER\_API,
      and PLUGIN\_PUBLISHER URLs with canonical port assignments
      Made-with: Cursor

    ***
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/21cfb0dc70d96022a9f539080d9de90dc862f5e3" iconLeft="github" />
</Update>

<Update label="fix(auth): enhance middleware and auth context for better..." tags={["Commit"]} rss={{ title: "NaaP: fix(auth): enhance middleware and auth context for better...", description: "fix(auth): enhance middleware and auth context for better session management (#199)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## fix(auth): enhance middleware and auth context for better...

  <ScrollBox maxHeight="150px" showHint={false}>
    fix(auth): enhance middleware and auth context for better session management (#199)

    * fix(auth): enhance middleware and auth context for better session management

    - Updated middleware to allow access to login pages when ?force=true is present, preventing redirect loops for authenticated users.
    - Improved cookie clearing logic in auth context to ensure compatibility across browsers by setting both max-age and expires attributes.
    - Refactored RequireAuth component to always clear auth storage before redirecting, addressing potential stale session issues.
      These changes improve the robustness of the authentication flow and enhance user experience during login attempts.
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/3a8930f36a24bd0174a1c4fe8e7477ae6970d90a" iconLeft="github" />
</Update>

<Update label="fix(auth): handle missing user data in auth context (#194)" tags={["Commit"]} rss={{ title: "NaaP: fix(auth): handle missing user data in auth context (#194)", description: "fix(auth): handle missing user data in auth context (#194)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## fix(auth): handle missing user data in auth context (#194)

  <ScrollBox maxHeight="150px" showHint={false}>
    fix(auth): handle missing user data in auth context (#194)

    * Handle missing user data in auth context
      Clear auth storage if user data is not present.
      Replace router.push with window\.location.replace to avoid redirect loops.
      Log a warning when the API returns a 200 status but no user data is present, prompting the clearing of stale authentication storage.
      Added `authErrorStatus` to the AuthState interface to manage error states during user authentication. Updated `fetchUser` to return user data along with the error status, improving error handling and preventing stale session issues. Adjusted the RequireAuth component to utilise the new error status for better session management.
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/ff96d3e3a00bd5bb372f34897744bc7a866adcec" iconLeft="github" />
</Update>

<Update label="fix(gateway): fix TDZ error preventing connector detail p..." tags={["Commit"]} rss={{ title: "NaaP: fix(gateway): fix TDZ error preventing connector detail p...", description: "fix(gateway): fix TDZ error preventing connector detail page from rendering (#197)" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## fix(gateway): fix TDZ error preventing connector detail p...

  <ScrollBox maxHeight="150px" showHint={false}>
    fix(gateway): fix TDZ error preventing connector detail page from rendering (#197)
    Move `connector` and `keys` declarations above the useEffect that
    references `connector?.endpoints` in its dependency array. The previous
    ordering caused a Temporal Dead Zone ReferenceError at runtime because
    the const was used before its lexical declaration.
    Made-with: Cursor
  </ScrollBox>

  <DoubleIconLink label="View commit on GitHub" href="https://github.com/livepeer/naap/commit/e117b13676b014adf854576b82111f5fdc23a0f3" iconLeft="github" />
</Update>
