Skip to content

Commit

Permalink
Merge pull request #8 from weissaufschwarz/fix-mitthooks
Browse files Browse the repository at this point in the history
Fix broken webhook handler
  • Loading branch information
freisenhauer authored Nov 27, 2024
2 parents 34a9983 + a3ca001 commit 456aa90
Show file tree
Hide file tree
Showing 27 changed files with 814 additions and 73 deletions.
5 changes: 5 additions & 0 deletions .changeset/khaki-penguins-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@weissaufschwarz/mitthooks": patch
---

BUGFIX: ignore casing of used webhook signature algorithm
5 changes: 5 additions & 0 deletions .changeset/shaggy-tigers-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@weissaufschwarz/mitthooks-nextjs": patch
---

BUGFIX: await request headers of nextjs webhook handler.
5 changes: 5 additions & 0 deletions .changeset/silly-tools-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@weissaufschwarz/mitthooks": patch
---

FEATURE: allow webhook handler without promises
3 changes: 2 additions & 1 deletion packages/mitthooks-nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export class NextJSWebhookHandler {
};

private async getWebhookContent(request: Request): Promise<WebhookContent> {
const headersList = headers();
// eslint-disable-next-line @typescript-eslint/await-thenable
const headersList = await headers();
const signatureSerial = this.getHeader(
headersList,
"x-marketplace-signature-serial",
Expand Down
9 changes: 6 additions & 3 deletions packages/mitthooks/src/examples/separateWebhookHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,22 @@ function createCustomSeparateWebhookHandler(
.build();
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
function createCustomSeparateWebhookHandlerWithHandlerForAdded(
extensionStorage: ExtensionStorage,
extensionId: string,
): SeparateWebhookHandlers {
const handlers = new SeparateWebhookHandlerFactory(extensionStorage, extensionId).build();

handlers.ExtensionAddedToContext = WebhookHandlerChain
const extendedChain = WebhookHandlerChain
.fromHandlerFunctions(
handlers.ExtensionAddedToContext,
async (webhookContent) => {
() => {
console.log("This is only gonna be called for ExtensionAddedToContext");
},
).handleWebhook;
)

handlers.ExtensionAddedToContext = extendedChain.handleWebhook.bind(extendedChain);

return handlers;
}
12 changes: 9 additions & 3 deletions packages/mitthooks/src/factory/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export abstract class BaseWebhookHandlerFactory {
protected baseHandlerChain: WebhookHandlerChain = new WebhookHandlerChain();
protected handlerChainSuffix: WebhookHandler[] = [];
protected verifyWebhookSignature = true;
protected mittwaldAPIURL: string | undefined;

protected constructor(extensionStorage: ExtensionStorage, extensionID: string) {
this.extensionStorage = extensionStorage;
Expand All @@ -36,6 +37,11 @@ export abstract class BaseWebhookHandlerFactory {
return this;
}

public withMittwaldAPIURL(url: string): this {
this.mittwaldAPIURL = url;
return this;
}

public withWebhookHandlerPrefix(
...additionalHandlers: WebhookHandler[]
): this {
Expand All @@ -59,7 +65,7 @@ export abstract class BaseWebhookHandlerFactory {

protected buildWebhookVerifier(): WebhookVerifier {
const publicKeyProvider = new CachingPublicKeyProvider(
APIPublicKeyProvider.newWithUnauthenticatedAPIClient(),
APIPublicKeyProvider.newWithUnauthenticatedAPIClient(this.mittwaldAPIURL)
);

return new WebhookVerifier(this.logger, publicKeyProvider);
Expand All @@ -75,13 +81,13 @@ export abstract class BaseWebhookHandlerFactory {

this.baseHandlerChain = this.baseHandlerChain.withAdditionalHandlers(
new LoggingWebhookHandler(this.logger),
new ExtensionIDVerificationWebhookHandler(this.extensionID),
new ExtensionIDVerificationWebhookHandler(this.extensionID, this.logger),
);

if (this.verifyWebhookSignature) {
this.baseHandlerChain =
this.baseHandlerChain.withAdditionalHandlers(
new VerifyingWebhookHandler(webhookVerifier),
new VerifyingWebhookHandler(webhookVerifier, this.logger),
);
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/mitthooks/src/factory/separate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,25 @@ export class SeparateWebhookHandlerFactory extends BaseWebhookHandlerFactory {
return {
[extensionAddedToContextKind]: this.baseHandlerChain
.withAdditionalHandlers(
new AddedToContextWebhookHandler(this.extensionStorage),
new AddedToContextWebhookHandler(this.extensionStorage, this.logger),
...this.handlerChainSuffix,
)
.handleWebhook.bind(this.baseHandlerChain),
[instanceUpdatedKind]: this.baseHandlerChain
.withAdditionalHandlers(
new InstanceUpdatedWebhookHandler(this.extensionStorage),
new InstanceUpdatedWebhookHandler(this.extensionStorage, this.logger),
...this.handlerChainSuffix,
)
.handleWebhook.bind(this.baseHandlerChain),
[secretRotatedKind]: this.baseHandlerChain
.withAdditionalHandlers(
new SecretRotatedWebhookHandler(this.extensionStorage),
new SecretRotatedWebhookHandler(this.extensionStorage, this.logger),
...this.handlerChainSuffix,
)
.handleWebhook.bind(this.baseHandlerChain),
[instanceRemovedKind]: this.baseHandlerChain
.withAdditionalHandlers(
new InstanceRemovedWebhookHandler(this.extensionStorage),
new InstanceRemovedWebhookHandler(this.extensionStorage, this.logger),
...this.handlerChainSuffix,
)
.handleWebhook.bind(this.baseHandlerChain),
Expand Down
22 changes: 15 additions & 7 deletions packages/mitthooks/src/handler/addedToContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import type { WebhookContent } from "../webhook.js";
import type { ExtensionAddedToContextWebhookBody } from "../schemas.js";
import { extensionAddedToContextWebhookSchema } from "../schemas.js";
import { InvalidBodyError } from "../errors.js";
import type {Logger} from "../logging/interface.js";

export class AddedToContextWebhookHandler implements WebhookHandler {
private readonly extensionStorage: ExtensionStorage;
private readonly logger: Logger;

public constructor(extensionStorage: ExtensionStorage) {
public constructor(extensionStorage: ExtensionStorage, logger: Logger) {
this.extensionStorage = extensionStorage;
this.logger = logger;
}

public async handleWebhook(
Expand All @@ -18,12 +21,17 @@ export class AddedToContextWebhookHandler implements WebhookHandler {
): Promise<void> {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);

await this.extensionStorage.upsertExtension({
extensionInstanceId: body.id,
contextId: body.context.id,
secret: body.secret,
consentedScopes: body.consentedScopes,
});
try {
await this.extensionStorage.upsertExtension({
extensionInstanceId: body.id,
contextId: body.context.id,
secret: body.secret,
consentedScopes: body.consentedScopes,
});
} catch (e) {
this.logger.error(`Failed to upsert extension: ${(e as Error).toString()}`);
throw e;
}

return next(webhookContent);
}
Expand Down
50 changes: 28 additions & 22 deletions packages/mitthooks/src/handler/combinedPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,34 @@ export class CombinedPersistenceWebhookHandler implements WebhookHandler {
next: HandleWebhook,
): Promise<void> {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);
switch (body.kind) {
case extensionAddedToContextKind:
await this.extensionStorage.upsertExtension({
extensionInstanceId: body.id,
contextId: body.context.id,
secret: body.secret,
consentedScopes: body.consentedScopes,
});
break;
case instanceUpdatedKind:
await this.extensionStorage.updateExtension({
extensionInstanceId: body.id,
contextId: body.context.id,
consentedScopes: body.consentedScopes,
enabled: body.state.enabled,
});
break;
case secretRotatedKind:
await this.extensionStorage.rotateSecret(body.id, body.secret);
break;
case instanceRemovedKind:
await this.extensionStorage.removeInstance(body.id);

try {
switch (body.kind) {
case extensionAddedToContextKind:
await this.extensionStorage.upsertExtension({
extensionInstanceId: body.id,
contextId: body.context.id,
secret: body.secret,
consentedScopes: body.consentedScopes,
});
break;
case instanceUpdatedKind:
await this.extensionStorage.updateExtension({
extensionInstanceId: body.id,
contextId: body.context.id,
consentedScopes: body.consentedScopes,
enabled: body.state.enabled,
});
break;
case secretRotatedKind:
await this.extensionStorage.rotateSecret(body.id, body.secret);
break;
case instanceRemovedKind:
await this.extensionStorage.removeInstance(body.id);
}
} catch (e) {
this.logger.error(`Failed to persist extension: ${(e as Error).toString()}`);
throw e;
}

await next(webhookContent);
Expand Down
19 changes: 14 additions & 5 deletions packages/mitthooks/src/handler/extensionId.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import type {HandleWebhook, WebhookHandler} from "./interface.js";
import type {WebhookContent} from "../webhook.js";
import {WebhookBody, webhookSchema,} from "../schemas.js";
import type {WebhookBody} from "../schemas.js";
import {webhookSchema} from "../schemas.js";
import {InvalidBodyError, InvalidExtensionIDError} from "../errors.js";
import type {Logger} from "../logging/interface.js";

export class ExtensionIDVerificationWebhookHandler implements WebhookHandler {
private readonly extensionID: string;
private readonly logger: Logger;

public constructor(extensionID: string) {
public constructor(extensionID: string, logger: Logger) {
this.extensionID = extensionID;
this.logger = logger;
}

public async handleWebhook(
webhookContent: WebhookContent,
next: HandleWebhook,
): Promise<void> {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);
if (body.meta.extensionId !== this.extensionID) {
throw new InvalidExtensionIDError(body.meta.extensionId)
try {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);
if (body.meta.extensionId !== this.extensionID) {
throw new InvalidExtensionIDError(body.meta.extensionId)
}
} catch (e) {
this.logger.error(`failed to verify extension id: ${(e as Error).toString()}`);
throw e;
}
return next(webhookContent);
}
Expand Down
14 changes: 11 additions & 3 deletions packages/mitthooks/src/handler/instanceRemoved.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,28 @@ import type { WebhookContent } from "../webhook.js";
import type { InstanceRemovedWebhookBody } from "../schemas.js";
import { instanceRemovedWebhookSchema } from "../schemas.js";
import { InvalidBodyError } from "../errors.js";
import type {Logger} from "../logging/interface.js";

export class InstanceRemovedWebhookHandler implements WebhookHandler {
private readonly extensionStorage: ExtensionStorage;
private readonly logger: Logger;

public constructor(extensionStorage: ExtensionStorage) {
public constructor(extensionStorage: ExtensionStorage, logger: Logger) {
this.extensionStorage = extensionStorage;
this.logger = logger;
}

public async handleWebhook(
webhookContent: WebhookContent,
next: HandleWebhook,
): Promise<void> {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);
await this.extensionStorage.removeInstance(body.id);
try {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);
await this.extensionStorage.removeInstance(body.id);
} catch (e) {
this.logger.error(`Failed to remove instance: ${(e as Error).toString()}`);
throw e;
}

return next(webhookContent);
}
Expand Down
24 changes: 16 additions & 8 deletions packages/mitthooks/src/handler/instanceUpdated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,34 @@ import type { WebhookContent } from "../webhook.js";
import type { InstanceUpdatedWebhookBody } from "../schemas.js";
import { instanceUpdatedWebhookSchema } from "../schemas.js";
import { InvalidBodyError } from "../errors.js";
import type {Logger} from "../logging/interface.js";

export class InstanceUpdatedWebhookHandler implements WebhookHandler {
private readonly extensionStorage: ExtensionStorage;
private readonly logger: Logger;

public constructor(extensionStorage: ExtensionStorage) {
public constructor(extensionStorage: ExtensionStorage, logger: Logger) {
this.extensionStorage = extensionStorage;
this.logger = logger;
}

public async handleWebhook(
webhookContent: WebhookContent,
next: HandleWebhook,
): Promise<void> {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);
try {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);

await this.extensionStorage.updateExtension({
extensionInstanceId: body.id,
contextId: body.context.id,
consentedScopes: body.consentedScopes,
enabled: body.state.enabled,
});
await this.extensionStorage.updateExtension({
extensionInstanceId: body.id,
contextId: body.context.id,
consentedScopes: body.consentedScopes,
enabled: body.state.enabled,
});
} catch (e) {
this.logger.error(`Failed to update extension: ${(e as Error).toString()}`);
throw e;
}

return next(webhookContent);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/mitthooks/src/handler/interface.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { WebhookContent } from "../webhook.js";

export type HandleWebhook = (webhookContent: WebhookContent) => Promise<void>;
export type HandleWebhook = (webhookContent: WebhookContent) => Promise<void> | void;

export interface WebhookHandler {
handleWebhook: (
webhookContent: WebhookContent,
next: HandleWebhook,
) => Promise<void>;
) => Promise<void> | void;
}
14 changes: 11 additions & 3 deletions packages/mitthooks/src/handler/secretRotated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,28 @@ import type { WebhookContent } from "../webhook.js";
import type { SecretRotatedWebhookBody } from "../schemas.js";
import { secretRotatedWebhookSchema } from "../schemas.js";
import { InvalidBodyError } from "../errors.js";
import type {Logger} from "../logging/interface.js";

export class SecretRotatedWebhookHandler implements WebhookHandler {
private readonly extensionStorage: ExtensionStorage;
private readonly logger: Logger;

public constructor(extensionStorage: ExtensionStorage) {
public constructor(extensionStorage: ExtensionStorage, logger: Logger) {
this.extensionStorage = extensionStorage;
this.logger = logger;
}

public async handleWebhook(
webhookContent: WebhookContent,
next: HandleWebhook,
): Promise<void> {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);
await this.extensionStorage.rotateSecret(body.id, body.secret);
try {
const body = this.getValidatedWebhookBody(webhookContent.rawBody);
await this.extensionStorage.rotateSecret(body.id, body.secret);
} catch (e) {
this.logger.error(`Failed to rotate secret: ${(e as Error).toString()}`);
throw e;
}

return next(webhookContent);
}
Expand Down
Loading

0 comments on commit 456aa90

Please sign in to comment.