Findings
2026-06-18 — API-key auth path + New Relic (NerdGraph) plugin
What was added#
ApiKeyConfig existed in packages/shared/src/types.ts from the start but no
apikey auth path was ever wired — ctx.http, the connect routes, and the
portal only handled oauth2 / cookie / none. Adding New Relic (which
authenticates with a User API Key + a region, no OAuth) required building the
generic apikey flow end-to-end first, then dropping the plugin on top.
The generic apikey flow#
- Manifest shape (
ApiKeyConfig):headerName(header the credential rides in) +fields[](connect-time form spec). Exactly one field setssecret: true— that's the credential. Fields withoptions[]render as a<select>. - Storage (
POST /api/auth/apikey/:integration, routes.ts): the secret field → encryptedaccessToken; every other field → per-connectionconfigJSON. Reuses the existingconnectionsrow +storeToken. NomarkConnected— apikey connect is synchronous (no PENDING record); the stored token alone makes/api/connectionsreport connected. ctx.httpapikey branch (plugins/context.ts): setsheaders[headerName] = accessTokenverbatim — noBearerprefix (New Relic wants a bareApi-Key: <key>). No token expiry/refresh. Bake any scheme into the field value if a future apikey integration needs one.- Host guard contract (added in PR #36 review): the apikey branch performs
no host validation unless the manifest sets
allowedHosts(optionalstring[]onApiKeyConfig). When present,ctx.httprejects any URL whose host isn't an exact/subdomain match before attaching the key — the apikey analogue of the cookie branch'scookieDomains. A plugin that forwards a user/tool-supplied URL toctx.httpmust setallowedHostsor guard the host itself, or it leaks the key to arbitrary hosts. New Relic pins["api.newrelic.com", "api.eu.newrelic.com"](all traffic is the hardcoded NerdGraph endpoint anyway). - Tools read config via
ctx.getConfig()[field.key](e.g.region), same mechanism self-hosted GitLab uses forinstanceUrl. - Portal:
GET /api/auth/:integrationreturns{ type: "apikey", fields }; Dashboard opensApiKeyAuthModal(renders the field spec) which POSTs the values back.isConfigured→ always true (user supplies the key; no server creds needed).
New Relic specifics#
- Everything is NerdGraph — one GraphQL endpoint, region-scoped:
US
https://api.newrelic.com/graphql, EUhttps://api.eu.newrelic.com/graphql. The reference tool list had two "Create Alert Policy" entries (one labelled "(GraphQL)"); they collapse to a singlenewrelic_create_alert_policysince there is no non-GraphQL path. - Reads (added after the create-only spec, since the plugin had no way to
read anything back):
newrelic_run_nrql(actor.account.nrql — the universal read),newrelic_search_entities(entitySearch; builds an ANDed query from name/domain/type, or takes a raw query — use it to find a dashboard/app GUID),newrelic_get_dashboard(actor.entity → DashboardEntity by GUID). - Confident mutations (well-documented):
alertsPolicyCreate,alertsNrqlConditionStaticCreate,taggingAddTagsToEntity,dashboardAddWidgetsToPage,aiNotificationsCreateDestination,aiNotificationsCreateChannel,aiWorkflowsCreateWorkflow,cloudConfigureIntegration. - Legacy / best-effort, UNTESTED against a live account:
alertsNotificationChannelCreateandalertsNotificationChannelsAddToPolicy. Legacy alert notification channels were historically a REST v2 surface; their NerdGraph mutation coverage is uncertain. New setups should prefer the AI Notifications destinations/channels/workflows. Verify these two against a real account before relying on them. - Dashboard widget input is flattened in the tool (title / visualizationId /
layout / nrqlQueries) and reassembled into NerdGraph's
visualization/layout/rawConfigurationshape. Cloud-integration and AI-notification auth blocks are provider-shaped, so those args are passthrough objects (z.record).