diff --git a/.changeset/fair-ads-trade.md b/.changeset/fair-ads-trade.md new file mode 100644 index 00000000..46f95c43 --- /dev/null +++ b/.changeset/fair-ads-trade.md @@ -0,0 +1,7 @@ +--- +"@saleor/app-sdk": minor +--- + +Fix wrong logic introduced in [0.49.0](https://github.com/saleor/app-sdk/releases/tag/v0.49.0): there is not header `saleor-schema-version` when app-sdk is processing saleor webhook. This header is only present on install request. + +Now app-sdk will try to parse version from `version` field on GraphQL subscription [Event](https://docs.saleor.io/docs/3.x/api-storefront/miscellaneous/interfaces/event#code-style-fontweight-normal-eventbversionbcodestring-). If field is not present `null` will be returned. diff --git a/src/handlers/next/saleor-webhooks/process-saleor-webhook.test.ts b/src/handlers/next/saleor-webhooks/process-saleor-webhook.test.ts index b8ca04c1..7be2291d 100644 --- a/src/handlers/next/saleor-webhooks/process-saleor-webhook.test.ts +++ b/src/handlers/next/saleor-webhooks/process-saleor-webhook.test.ts @@ -45,7 +45,6 @@ describe("processAsyncSaleorWebhook", () => { "saleor-event": "product_updated", "saleor-signature": "mocked_signature", "content-length": "0", // is ignored by mocked raw-body. - "saleor-schema-version": "3.19", }, method: "POST", // body can be skipped because we mock it with raw-body @@ -173,26 +172,4 @@ describe("processAsyncSaleorWebhook", () => { schemaVersion: null, }); }); - - it("Return schema version if saleor-schema-version header is present", async () => { - await expect( - processSaleorWebhook({ - req: mockRequest, - apl: mockAPL, - allowedEvent: "PRODUCT_UPDATED", - }) - ).resolves.toStrictEqual({ - authData: { - appId: "mock-app-id", - domain: "example.com", - jwks: "{}", - saleorApiUrl: "https://example.com/graphql/", - token: "mock-token", - }, - baseUrl: "https://some-saleor-host.cloud", - event: "product_updated", - payload: {}, - schemaVersion: 3.19, - }); - }); }); diff --git a/src/handlers/next/saleor-webhooks/process-saleor-webhook.ts b/src/handlers/next/saleor-webhooks/process-saleor-webhook.ts index 619da72b..d0419255 100644 --- a/src/handlers/next/saleor-webhooks/process-saleor-webhook.ts +++ b/src/handlers/next/saleor-webhooks/process-saleor-webhook.ts @@ -8,6 +8,7 @@ import { createDebug } from "../../../debug"; import { fetchRemoteJwks } from "../../../fetch-remote-jwks"; import { getBaseUrl, getSaleorHeaders } from "../../../headers"; import { getOtelTracer } from "../../../open-telemetry"; +import { parseSchemaVersion } from "../../../util"; import { verifySignatureWithJwks } from "../../../verify-signature"; const debug = createDebug("processSaleorWebhook"); @@ -89,7 +90,7 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async ({ throw new WebhookError("Wrong request method, only POST allowed", "WRONG_METHOD"); } - const { event, signature, saleorApiUrl, schemaVersion } = getSaleorHeaders(req.headers); + const { event, signature, saleorApiUrl } = getSaleorHeaders(req.headers); const baseUrl = getBaseUrl(req.headers); if (!baseUrl) { @@ -107,10 +108,6 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async ({ throw new WebhookError("Missing saleor-event header", "MISSING_EVENT_HEADER"); } - if (!schemaVersion) { - debug("Missing saleor-schema-version header"); - } - const expected = allowedEvent.toLowerCase(); if (event !== expected) { @@ -140,7 +137,7 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async ({ throw new WebhookError("Missing request body", "MISSING_REQUEST_BODY"); } - let parsedBody: unknown; + let parsedBody: unknown & { version?: string | null }; try { parsedBody = JSON.parse(rawBody); @@ -150,6 +147,14 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async ({ throw new WebhookError("Request body can't be parsed", "CANT_BE_PARSED"); } + let parsedSchemaVersion: number | null = null; + + try { + parsedSchemaVersion = parseSchemaVersion(parsedBody.version); + } catch { + debug("Schema version cannot be parsed"); + } + /** * Verify if the app is properly installed for given Saleor API URL */ @@ -215,7 +220,7 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async ({ event, payload: parsedBody as T, authData, - schemaVersion, + schemaVersion: parsedSchemaVersion, }; } catch (err) { const message = (err as Error)?.message ?? "Unknown error"; diff --git a/src/util/index.ts b/src/util/index.ts index deacb276..fe096549 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,2 +1,3 @@ export * from "./is-in-iframe"; +export * from "./schema-version"; export * from "./use-is-mounted"; diff --git a/src/util/schema-version.test.ts b/src/util/schema-version.test.ts new file mode 100644 index 00000000..b850efaf --- /dev/null +++ b/src/util/schema-version.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "vitest"; + +import { parseSchemaVersion } from "./schema-version"; + +describe("parseSchemaVersion", () => { + test.each([ + { + rawVersion: "3", + parsedVersion: null, + }, + { + rawVersion: "3.19", + parsedVersion: 3.19, + }, + { + rawVersion: "3.19.1", + parsedVersion: 3.19, + }, + { + rawVersion: "malformed", + parsedVersion: null, + }, + { + rawVersion: "malformed.raw", + parsedVersion: null, + }, + { + rawVersion: "malformed.raw.version", + parsedVersion: null, + }, + { + rawVersion: null, + parsedVersion: null, + }, + { + rawVersion: undefined, + parsedVersion: null, + }, + ])( + "Parses version string from: $rawVersion to: $parsedVersion", + ({ rawVersion, parsedVersion }) => { + expect(parseSchemaVersion(rawVersion)).toBe(parsedVersion); + } + ); +}); diff --git a/src/util/schema-version.ts b/src/util/schema-version.ts new file mode 100644 index 00000000..d1201c30 --- /dev/null +++ b/src/util/schema-version.ts @@ -0,0 +1,15 @@ +export const parseSchemaVersion = (rawVersion: string | undefined | null): number | null => { + if (!rawVersion) { + return null; + } + + const [majorString, minorString] = rawVersion.split("."); + const major = parseInt(majorString, 10); + const minor = parseInt(minorString, 10); + + if (major && minor) { + return parseFloat(`${major}.${minor}`); + } + + return null; +};