Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/petite-days-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@saleor/webhook-utils": minor
---

Added APP_DELETED shared webhook that automatically cleans up APL
1 change: 1 addition & 0 deletions apps/klaviyo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@opentelemetry/semantic-conventions": "catalog:",
"@saleor/app-sdk": "link:../../node_modules/@saleor/app-sdk",
"@saleor/apps-logger": "workspace:*",
"@saleor/webhook-utils": "workspace:*",
"@saleor/apps-otel": "workspace:*",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
Expand Down
4 changes: 3 additions & 1 deletion apps/klaviyo/src/pages/api/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
import { AppManifest } from "@saleor/app-sdk/types";
import { type AppManifest } from "@saleor/app-sdk/types";
import { wrapWithLoggerContext } from "@saleor/apps-logger/node";
import { withSpanAttributes } from "@saleor/apps-otel/src/with-span-attributes";

import pkg from "../../../package.json";
import { env } from "../../env";
import { loggerContext } from "../../logger-context";
import { getWebhookManifest as getAppDeletedWebhookManifest } from "./webhooks/app-deleted";
import { customerCreatedWebhook } from "./webhooks/customer-created";
import { fulfillmentCreatedWebhook } from "./webhooks/fulfillment-created";
import { orderCreatedWebhook } from "./webhooks/order-created";
Expand Down Expand Up @@ -41,6 +42,7 @@ const handler = wrapWithLoggerContext(
fulfillmentCreatedWebhook.getWebhookManifest(appBaseUrl),
orderCreatedWebhook.getWebhookManifest(appBaseUrl),
orderFullyPaidWebhook.getWebhookManifest(appBaseUrl),
getAppDeletedWebhookManifest(appBaseUrl),
],
};
},
Expand Down
20 changes: 20 additions & 0 deletions apps/klaviyo/src/pages/api/webhooks/app-deleted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createAppDeletedHandler } from "@saleor/webhook-utils/app-deleted-handler";

import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../logger";

const { handler, getWebhookManifest } = createAppDeletedHandler({
apl: saleorApp.apl,
logger: createLogger("APP_DELETED handler"),
webhookPath: "api/webhooks/app-deleted",

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the other webhook routes in this app, this handler is not wrapped with wrapWithLoggerContext and withSpanAttributes, so logs/traces for APP_DELETED may miss the usual context and OpenTelemetry attributes. Consider wrapping the exported handler consistently (or have createAppDeletedHandler expose a wrapped handler option).

Copilot uses AI. Check for mistakes.
hooks: {
onEvent: console.log,
onAuthDataDeleted: console.log,
onAuthDataDeleteError: console.log,
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hooks.onEvent: console.log will likely print the entire WebhookContext, which includes authData (e.g. token) in other webhook handlers. This is a sensitive-data leakage risk; avoid logging the full context and log only non-sensitive fields via the app logger (or remove these hooks from the production route).

Suggested change
const { handler, getWebhookManifest } = createAppDeletedHandler({
apl: saleorApp.apl,
logger: createLogger("APP_DELETED handler"),
webhookPath: "api/webhooks/app-deleted",
hooks: {
onEvent: console.log,
onAuthDataDeleted: console.log,
onAuthDataDeleteError: console.log,
const logger = createLogger("APP_DELETED handler");
const { handler, getWebhookManifest } = createAppDeletedHandler({
apl: saleorApp.apl,
logger,
webhookPath: "api/webhooks/app-deleted",
hooks: {
onEvent: () => logger.info("App deleted webhook event received"),
onAuthDataDeleted: () => logger.info("Auth data deleted after app removal"),
onAuthDataDeleteError: () => logger.error("Failed to delete auth data after app removal"),

Copilot uses AI. Check for mistakes.
},
});

export { getWebhookManifest };

export default handler;
23 changes: 22 additions & 1 deletion packages/webhook-utils/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,11 @@ export type GetSaleorInstanceDataQueryVariables = Exact<{ [key: string]: never;

export type GetSaleorInstanceDataQuery = { readonly shop: { readonly version: string } };

export type AppDeletedSubscriptionVariables = Exact<{ [key: string]: never; }>;


export type AppDeletedSubscription = { readonly event?: { readonly app?: { readonly id: string } | null } | {} | null };

export const UntypedWebhookDetailsFragmentFragmentDoc = gql`
fragment WebhookDetailsFragment on Webhook {
id
Expand Down Expand Up @@ -960,9 +965,25 @@ export const UntypedGetSaleorInstanceDataDocument = gql`
export function useGetSaleorInstanceDataQuery(options?: Omit<Urql.UseQueryArgs<GetSaleorInstanceDataQueryVariables>, 'query'>) {
return Urql.useQuery<GetSaleorInstanceDataQuery, GetSaleorInstanceDataQueryVariables>({ query: UntypedGetSaleorInstanceDataDocument, ...options });
};
export const UntypedAppDeletedDocument = gql`
subscription AppDeleted {
event {
... on AppDeleted {
app {
id
}
}
}
}
`;

export function useAppDeletedSubscription<TData = AppDeletedSubscription>(options?: Omit<Urql.UseSubscriptionArgs<AppDeletedSubscriptionVariables>, 'query'>, handler?: Urql.SubscriptionHandler<AppDeletedSubscription, TData>) {
return Urql.useSubscription<AppDeletedSubscription, TData, AppDeletedSubscriptionVariables>({ query: UntypedAppDeletedDocument, ...options }, handler);
};
export const WebhookDetailsFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WebhookDetailsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Webhook"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"targetUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subscriptionQuery"}},{"kind":"Field","name":{"kind":"Name","value":"syncEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"asyncEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}}]}}]} as unknown as DocumentNode<WebhookDetailsFragmentFragment, unknown>;
export const CreateAppWebhookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateAppWebhook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WebhookCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhookCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhook"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WebhookDetailsFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WebhookDetailsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Webhook"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"targetUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subscriptionQuery"}},{"kind":"Field","name":{"kind":"Name","value":"syncEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"asyncEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}}]}}]} as unknown as DocumentNode<CreateAppWebhookMutation, CreateAppWebhookMutationVariables>;
export const ModifyAppWebhookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ModifyAppWebhook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WebhookUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhookUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"Field","name":{"kind":"Name","value":"webhook"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WebhookDetailsFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WebhookDetailsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Webhook"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"targetUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subscriptionQuery"}},{"kind":"Field","name":{"kind":"Name","value":"syncEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"asyncEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}}]}}]} as unknown as DocumentNode<ModifyAppWebhookMutation, ModifyAppWebhookMutationVariables>;
export const RemoveAppWebhookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveAppWebhook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhookDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhook"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]} as unknown as DocumentNode<RemoveAppWebhookMutation, RemoveAppWebhookMutationVariables>;
export const GetAppDetailsAndWebhooksDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppDetailsAndWebhooksData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"appUrl"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"webhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WebhookDetailsFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WebhookDetailsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Webhook"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"targetUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subscriptionQuery"}},{"kind":"Field","name":{"kind":"Name","value":"syncEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"asyncEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}}]}}]} as unknown as DocumentNode<GetAppDetailsAndWebhooksDataQuery, GetAppDetailsAndWebhooksDataQueryVariables>;
export const GetSaleorInstanceDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSaleorInstanceData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"shop"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]} as unknown as DocumentNode<GetSaleorInstanceDataQuery, GetSaleorInstanceDataQueryVariables>;
export const GetSaleorInstanceDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSaleorInstanceData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"shop"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]} as unknown as DocumentNode<GetSaleorInstanceDataQuery, GetSaleorInstanceDataQueryVariables>;
export const AppDeletedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"AppDeleted"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AppDeleted"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode<AppDeletedSubscription, AppDeletedSubscriptionVariables>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
subscription AppDeleted {
event {
... on AppDeleted {
app {
id
}
}
}
}
5 changes: 5 additions & 0 deletions packages/webhook-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"name": "@saleor/webhook-utils",
"version": "0.2.10",
"type": "module",
"exports": {
".": "./index.ts",
"./app-deleted-handler": "./src/app-deleted-handler.ts"
},
"main": "index.ts",
"scripts": {
"check-types": "tsc",
Expand Down Expand Up @@ -38,6 +42,7 @@
"vitest": "catalog:"
},
"peerDependencies": {
"@saleor/apps-logger": "workspace:*",
"next": "catalog:",
"urql": "catalog:"
}
Expand Down
60 changes: 60 additions & 0 deletions packages/webhook-utils/src/app-deleted-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { type APL } from "@saleor/app-sdk/APL";
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { type WebhookContext } from "@saleor/app-sdk/handlers/shared";
import { type Logger } from "@saleor/apps-logger";

import { AppDeletedDocument } from "../generated/graphql";

type Params = {
apl: APL;
webhookPath: string;
logger: Logger;
Comment on lines +4 to +11
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new API requires Logger from @saleor/apps-logger, but the package already defines its own Logger type in src/types.ts (used by other exports). Having two incompatible logger types in the same package is confusing for consumers; consider accepting a minimal logger interface (info/warn/error/debug) or reusing the existing src/types.ts Logger shape and letting apps pass their logger adapter.

Suggested change
import { type Logger } from "@saleor/apps-logger";
import { AppDeletedDocument } from "../generated/graphql";
type Params = {
apl: APL;
webhookPath: string;
logger: Logger;
import { AppDeletedDocument } from "../generated/graphql";
type MinimalLogger = {
info: (message: string, ...meta: unknown[]) => void;
warn?: (message: string, ...meta: unknown[]) => void;
error: (message: string, ...meta: unknown[]) => void;
debug?: (message: string, ...meta: unknown[]) => void;
};
type Params = {
apl: APL;
webhookPath: string;
logger: MinimalLogger;

Copilot uses AI. Check for mistakes.
hooks?: {
onEvent?: (ctx: WebhookContext<unknown>) => void;
onAuthDataDeleted?: () => void;
onAuthDataDeleteError?: (e: Error) => void;
};
};

/**
* TODO:
* 1. Move to app-sdk
* 2. Implement into non-monorepo apps
*/
export const createAppDeletedHandler = ({ apl, webhookPath, hooks = {}, logger }: Params) => {
const webhook = new SaleorAsyncWebhook({
apl,
name: "APP_DELETED",
query: AppDeletedDocument,
Comment on lines +24 to +28
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces new webhook-handling behavior (creating a SaleorAsyncWebhook and deleting APL entries) but there are no unit tests covering the success/error paths. Since this package already uses Vitest tests, add tests that verify apl.delete is called with the Saleor API URL and that the handler returns 200 on success and 500 on failure.

Copilot uses AI. Check for mistakes.
event: "APP_DELETED",
isActive: true,
webhookPath,
Comment on lines +25 to +31
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webhook name is set to the all-caps event identifier ("APP_DELETED"). In other apps, webhook name is a human-readable label (e.g. "OrderCancelled", "Customer Created") and shows up in Saleor’s webhook list. Consider changing this to a descriptive name like "App deleted" / "AppDeleted" while keeping event: "APP_DELETED".

Copilot uses AI. Check for mistakes.
});

const handler = webhook.createHandler(async (_req, res, ctx) => {
// todo something failing here
console.log("asdf");

Check failure on line 36 in packages/webhook-utils/src/app-deleted-handler.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the leftover debug console.log("asdf") (and the adjacent TODO). This will pollute logs in production and bypasses the app logger/observability pipeline used elsewhere in the repo.

Suggested change
// todo something failing here
console.log("asdf");

Copilot uses AI. Check for mistakes.
logger.info("APP_DELETED event received. Auth Data will be removed");

hooks.onEvent?.(ctx);

try {
await apl.delete(ctx.authData.saleorApiUrl);

hooks.onAuthDataDeleted?.();

return res.status(200).end();
} catch (e) {
logger.error("Error deleting auth data on APP_DELETED", e);

hooks.onAuthDataDeleteError?.(e as Error);

return res.status(500).send("Failed to clean up auth data.");
}
});
Comment on lines +24 to +58
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new shared handler introduces non-trivial behavior (invoking hooks, deleting APL auth data, and mapping failures to HTTP responses) but there are no unit tests covering success/failure paths. Add tests for: (1) calling apl.delete with ctx.authData.saleorApiUrl, (2) hook invocation order, and (3) 500 responses + onAuthDataDeleteError on deletion failure.

Copilot uses AI. Check for mistakes.

return {
handler: handler.bind(webhook),
getWebhookManifest: webhook.getWebhookManifest.bind(webhook),
};
};
Loading
Loading