nv-endpoint-routed-tool-provider ناجح

Build or refactor a Tool-channel provider (PagerDuty, Opsgenie, future incident/alerting tools) to be endpoint-routed: per-subscriber secrets stored encrypted on a ChannelConnection behind a typed channel endpoint, a stateless provider that resolves routing from channelData at send time, SKIPPED steps when no endpoint exists, and the full API/worker/dashboard/docs/playground surface. Starts with a mandatory provider-docs discovery gate. Use when refactoring Opsgenie to the PagerDuty model, adding a new tool provider, or touching pagerduty_service channel endpoints.

٣٩.٤k
نجوم
٠
تنزيلات
٢
مشاهدات

// تثبيت المهارة

تثبيت المهارة

المهارات هي كود تابع لأطراف ثالثة من مستودعات GitHub العامة. يفحص SkillHub الأنماط الخبيثة المعروفة، لكنه لا يستطيع ضمان السلامة. راجع الكود المصدري قبل التثبيت.

تثبيت عام (على مستوى المستخدم):

npx skillhub install novuhq/novu/nv-endpoint-routed-tool-provider

تثبيت في المشروع الحالي:

npx skillhub install novuhq/novu/nv-endpoint-routed-tool-provider --project

skill.install.customTargetHelp

npx skillhub install novuhq/novu/nv-endpoint-routed-tool-provider --target-dir /path/to/skills

المسار المقترح: ~/.claude/skills/nv-endpoint-routed-tool-provider/

محتوى SKILL.md

---
name: nv-endpoint-routed-tool-provider
description: >-
  Build or refactor a Tool-channel provider (PagerDuty, Opsgenie, future
  incident/alerting tools) to be endpoint-routed: per-subscriber secrets stored
  encrypted on a ChannelConnection behind a typed channel endpoint, a stateless
  provider that resolves routing from channelData at send time, SKIPPED steps
  when no endpoint exists, and the full API/worker/dashboard/docs/playground
  surface. Starts with a mandatory provider-docs discovery gate. Use when
  refactoring Opsgenie to the PagerDuty model, adding a new tool provider, or
  touching pagerduty_service channel endpoints.
---

# Endpoint-Routed Tool Provider (the PagerDuty model)

PagerDuty is the reference implementation. "Endpoint-routed" means the provider
has **no environment-level credentials**: every subscriber registers their own
secret (e.g. a PagerDuty Events API v2 routing key) as a channel endpoint, and
one trigger fans out to a different external service per subscriber. Grep for
`pagerduty_service` / `ENDPOINT_ROUTED_TOOL_PROVIDERS` to find every touch
point of the reference implementation.

## Step 0: Provider discovery gate (mandatory, before any code)

Every invariant and checklist item below encodes a fact from PagerDuty's
Events API v2 docs (32-char key regex, us/eu endpoints, `dedup_key`, 1024-char
summary limit). Do not transplant those facts onto another provider. First,
read the target provider's official API docs and pin down, with citations:

- **Per-recipient secret**: name, format (drives the validator regex and DTO
  `@Matches`), and scope. It must be issuable per recipient/service.
- **Regional endpoints**: the `region` equivalent, or none.
- **Idempotency**: the `dedup_key` equivalent and its retry semantics.
- **Payload contract**: required fields, size limits, allowed actions and
  severities (drives defaults, `RESERVED_OVERRIDE_KEYS`, and truncation).
- **Auth transport**: secret in body vs header.

Then apply the gate:

- **Open question that changes the design** (secret scope, missing
  idempotency, OAuth-only auth, ...): ask the user with concrete options. Do
  not assume.
- **An architecture invariant cannot be satisfied** (e.g. no per-recipient
  secret exists): halt. Explain which invariant fails and name the
  alternatives (credential-routed tool provider, OAuth connection model)
  instead of forcing this pattern.
- **All questions answered**: record the findings (they become the validator,
  provider defaults, and docs content) and proceed to the checklist.

## Architecture invariants (do not renegotiate these)

1. **Secret lives on the connection, not the endpoint.** The wire shape (e.g.
   `{ routingKey, region }`) is accepted and returned by the API, but persisted
   encrypted on a dedicated `ChannelConnection.auth` (1:1 owned by the
   endpoint). The stored `ChannelEndpoint.endpoint` document is `{}`. Read
   paths re-hydrate the wire shape by decrypting the linked connection.
2. **Exactly one endpoint per (environment, subscriber, integration)**,
   enforced by a Mongo partial unique index on the endpoint `type`. Duplicate
   `POST` → `409 Conflict`; rotation is a `PATCH` on the existing endpoint.
3. **The provider is stateless.** Constructor takes no credentials. Routing is
   resolved inside `sendMessage` from `options.channelData.endpoint`, guarded
   by `isChannelDataOfType(channelData, ENDPOINT_TYPES.X)`. Missing/wrong
   channelData throws.
4. **No endpoint → step is SKIPPED**, never errored. The worker send loop
   checks `ENDPOINT_ROUTED_TOOL_PROVIDERS` (a `Set` in
   `send-message-tool.usecase.ts`); credential-routed tool providers
   (tool-webhook, Opsgenie today) still fall back to `integration.credentials`.
5. **Deterministic `dedup_key`** from `transactionId + subscriberId + stepId`
   so worker retries update the same incident instead of duplicating it. An
   explicit override wins.
6. **Secrets never leak**: not in logs, execution details, or message
   documents. The routing key is added to `SECURE_AUTH_FIELDS` in
   `encrypt-channel-connection-auth.ts` so it is encrypted at rest.
7. **`createSubscriberIfMissing`** (optional boolean on the create-endpoint
   body) JIT-creates the subscriber via `CreateOrUpdateSubscriberUseCase` with
   `allowUpdate: false` (create-only, never mutates). Without it, unknown
   subscriberId → 404 whose message names the flag. Connect surfaces are often
   the user's first Novu touchpoint, so client guides should pass `true`.

## Implementation checklist (slice order matters)

Only start after the discovery gate has passed. Work bottom-up; each slice
must build before the next (see Build order below).

**A. Foundations (`packages/shared`, `packages/stateless`, `libs/dal`, `libs/application-generic`)**
- [ ] Add `X_TYPE` to `ENDPOINT_TYPES` + wire shape in `ChannelEndpointByType` — `packages/shared/src/types/channel-endpoint.ts`
- [ ] Mirror both in `packages/stateless/src/lib/provider/channel-data.type.ts`; add the `XData` type to `ChannelData` and to `ENDPOINT_TYPES_REQUIRING_TOKEN`
- [ ] Partial unique index in `libs/dal/src/repositories/channel-endpoint/channel-endpoint.schema.ts` (`{ _environmentId, subscriberId, integrationIdentifier, type }`, `partialFilterExpression: { type }`)
- [ ] Add secret field(s) to `SECURE_AUTH_FIELDS` + `ChannelConnectionAuth` in `libs/application-generic/src/encryption/encrypt-channel-connection-auth.ts` (+ spec)
- [ ] Validator entry in `libs/application-generic/src/schemas/channel-endpoint/channel-endpoint.schema.ts` (format-validate the secret, e.g. `/^[a-zA-Z0-9]{32}$/`, and reject extra keys)

**B. Provider (`packages/providers`, `libs/application-generic`)**
- [ ] Stateless provider in `packages/providers/src/lib/tool/<provider>/` — resolve routing from `channelData`, deterministic dedup, reserved-override handling (+ spec)
- [ ] Handler in `libs/application-generic/src/factories/tool/handlers/` — `buildProvider` ignores `ICredentials`
- [ ] Empty credentials config in `packages/shared/src/consts/providers/credentials/provider-credentials.ts` (`export const xConfig: IConfigCredential[] = []`) with a comment pointing at the channel-endpoints API

**C. API (`apps/api/src/app/channel-endpoints`)**
- [ ] Endpoint DTO in `dtos/endpoint-types.dto.ts` (regex-validate the secret) + `Create<X>EndpointDto` in `dtos/create-channel-endpoint-variants.dto.ts`
- [ ] Controller: `@ApiExtraModels` + `oneOf` + discriminator mapping for the new DTO
- [ ] Create usecase: connection-first then endpoint, compensating delete on failure, duplicate-key → 409; update usecase rotates on the connection; delete cascades the connection; get/list hydrate the wire shape (batch `$in` for list)
- [ ] e2e in `e2e/create-channel-endpoint.e2e.ts`: happy path, 409, rotation, `createSubscriberIfMissing` (404 hint / JIT create / no-mutation)

**D. Worker (`apps/worker/src/app/workflow/usecases/send-message`)**
- [ ] `resolve-channel-endpoints.usecase.ts`: extract + decrypt the secret from the connection into `channelData.endpoint`
- [ ] `send-message-tool.usecase.ts`: add the provider id to `ENDPOINT_ROUTED_TOOL_PROVIDERS`; verify SKIPPED + execution detail on missing endpoint (+ spec for the predicate)

**E. Surface (dashboard, docs, playground)**
- [ ] Provider const in `packages/shared/src/consts/providers/channels/tool.ts` with `docReference` → `https://docs.novu.co/platform/integrations/tool/<provider>` (rebuild `@novu/shared` after)
- [ ] Docs guide `docs/platform/integrations/tool/<provider>.mdx` + register in the `Tool` group of `docs/docs.json` — see [reference.md](reference.md) for the required page structure
- [ ] Playground demo under `playground/nextjs` mirroring the PagerDuty trio: `src/lib/<provider>-endpoint-connect.ts`, `src/pages/api/<provider>-endpoint.ts`, `src/components/<provider>-end-user-connect.tsx` + Clerk-gated page + SideNav entry

For per-file code patterns (index definition, provider skeleton, usecase
transaction shape, DTO union, worker extraction, playground helper contract),
see [reference.md](reference.md).

## Opsgenie refactor note

Opsgenie is credential-routed today (env-level `apiKey`). Refactoring it to
this model = running the checklist with `opsgenie_*` as the endpoint type and
**removing** it from the credential-routed fallback: empty `opsgenieConfig`,
drop the API-key read in `opsgenie.handler.ts`, add it to
`ENDPOINT_ROUTED_TOOL_PROVIDERS`. PagerDuty was never released so it had no
migration; Opsgenie may need one — check for existing integrations with
credentials before deleting the old path. The discovery gate still applies:
confirm Opsgenie's Alert API supports per-recipient API keys and idempotent
alert deduplication before assuming parity with PagerDuty.

## Build order & verification

```bash
# types flow stateless → providers → application-generic; skipping a step
# yields phantom TS errors (missing channelData on IToolOptions, etc.)
pnpm --filter @novu/shared build
pnpm --filter @novu/stateless build
pnpm --filter @novu/providers build
pnpm --filter @novu/application-generic build

# provider unit tests
CI=true pnpm --filter @novu/providers exec vitest run src/lib/tool/<provider>

# channel-endpoints e2e (from apps/api)
pnpm exec cross-env NODE_ENV=test CI_EE_TEST=true CLERK_ENABLED=true \
  NODE_OPTIONS=--max_old_space_size=8192 mocha --timeout 30000 --retries 3 \
  --grep '#novu-v2' --require ./swc-register.js --exit --file e2e/setup.ts \
  'src/**/create-channel-endpoint.e2e{,-ee}.ts'
```

## Gotchas learned the hard way

- **Module DI**: JIT subscriber creation needs `SharedModule` +
  `CreateOrUpdateSubscriberUseCase` + `UpdateSubscriber` +
  `UpdateSubscriberChannel` as providers in `channel-endpoints.module.ts`
  (mirror `channel-connections.module.ts`).
- **Worker/app-generic specs need `STORE_ENCRYPTION_KEY`** (32 chars) in the
  env or `encryptChannelConnectionAuth` throws a Buffer TypeError.
- **`@novu/api` SDK lags**: until the OpenAPI regen runs, the internal SDK's
  create-endpoint union won't include the new DTO — playground/demo code calls
  the raw REST endpoint (`novuFetch` pattern) and swaps to the SDK later.
  Never edit `libs/internal-sdk` by hand.
- **Dashboard View Guide CTA**: zero-credential providers skip the credentials
  accordion; the docs CTA in `integration-settings.tsx` must live outside the
  `providerCredentials.length > 0` block or it never renders.
- **Dashboard gating**: the Tool channel is behind the
  `IS_TOOL_CHANNEL_ENABLED` LaunchDarkly flag — check it before debugging a
  "missing provider" in the integration store.
- **Secrets in reads**: Novu returns secrets on read and masks client-side
  (last 4 chars, `••••XXXX`) — follow that convention, don't invent
  server-side masking.
- **Writing style**: no em dashes (`—`) in this feature's docs/UI copy; use
  periods, colons, or parentheses.

الترخيص

لم يُعلَن أي ترخيص في مصدر هذه المهارة.

النص الكامل للترخيص متاح في المستودع المصدري.

عرض الترخيص في المستودع المصدريالنسخة المنشورة هناك هي المرجع.