@team-plain/webhooks 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,7 +84,9 @@ function verifyWebhookSignature(payload, signature, secret) {
84
84
  };
85
85
  }
86
86
  const expectedSignature = crypto.createHmac("sha256", secret).update(payload, "utf8").digest("hex");
87
- if (signature !== expectedSignature) {
87
+ const given = Buffer.from(signature, "utf8");
88
+ const expected = Buffer.from(expectedSignature, "utf8");
89
+ if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {
88
90
  return {
89
91
  error: new import_errors.PlainWebhookSignatureVerificationError("The signature provided is invalid.")
90
92
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/verify.ts"],"sourcesContent":["import * as crypto from \"node:crypto\";\nimport {\n type PlainWebhookError,\n PlainWebhookPayloadError,\n PlainWebhookSignatureVerificationError,\n} from \"./errors.js\";\nimport { parsePlainWebhook } from \"./parse.js\";\nimport { isErr, type Result } from \"./result.js\";\nimport type { WebhooksSchemaDefinition } from \"./webhook-schema.js\";\n\n/**\n * Verifies and validates a Plain webhook payload.\n *\n * @param payload - The raw request body.\n * @param signature - The value of the 'Plain-Request-Signature' header.\n * @param secret - Your Plain webhook signing secret.\n * @param tolerance - (Optional) Maximum allowed difference in seconds between the timestamp and the current time. Defaults to 300 seconds. This is helpful to prevent replay attacks.\n * @returns A Result object containing the validated payload or an error.\n */\nexport function verifyPlainWebhook(\n payloadRaw: string,\n signature: string,\n secret: string,\n tolerance = 300, // 5 minutes\n): Result<WebhooksSchemaDefinition, PlainWebhookError> {\n const signatureVerificationResult = verifyWebhookSignature(payloadRaw, signature, secret);\n if (isErr(signatureVerificationResult)) {\n return signatureVerificationResult;\n }\n\n const payloadResult = parsePlainWebhook(payloadRaw);\n if (isErr(payloadResult)) {\n return payloadResult;\n }\n\n const payload = payloadResult.data;\n const timestamp = Date.parse(payload.webhookMetadata.webhookDeliveryAttemptTimestamp);\n\n if (Number.isNaN(timestamp)) {\n return {\n error: new PlainWebhookPayloadError(\n `Invalid timestamp provided in the webhook payload: ${timestamp}. This is likely a bug in the Plain API (eventId: ${payload.id}).`,\n ),\n };\n }\n\n const currentTime = Date.now();\n if (Math.abs(currentTime - timestamp) > tolerance * 1000) {\n return {\n error: new PlainWebhookSignatureVerificationError(\n `The timestamp provided in the webhook payload is too far in the past. The maximum allowed difference is ${tolerance} seconds.`,\n ),\n };\n }\n\n return { data: payload };\n}\n\nfunction verifyWebhookSignature(\n payload: string,\n signature: string,\n secret: string,\n): Result<true, PlainWebhookSignatureVerificationError> {\n if (payload.length === 0) {\n return {\n error: new PlainWebhookSignatureVerificationError(\"No webhook payload was provided.\"),\n };\n }\n\n if (signature.length === 0) {\n return {\n error: new PlainWebhookSignatureVerificationError(\n 'No signature header value was provided. Please pass the value of the \"Plain-Request-Signature\" header.',\n ),\n };\n }\n\n if (secret.length === 0) {\n return {\n error: new PlainWebhookSignatureVerificationError(\n \"No webhook secret was provided. You can find your webhook secret in your workspace settings.\",\n ),\n };\n }\n\n const expectedSignature = crypto\n .createHmac(\"sha256\", secret)\n .update(payload, \"utf8\")\n .digest(\"hex\");\n\n if (signature !== expectedSignature) {\n return {\n error: new PlainWebhookSignatureVerificationError(\"The signature provided is invalid.\"),\n };\n }\n\n return { data: true };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,aAAwB;AACxB,oBAIO;AACP,mBAAkC;AAClC,oBAAmC;AAY5B,SAAS,mBACd,YACA,WACA,QACA,YAAY,KACyC;AACrD,QAAM,8BAA8B,uBAAuB,YAAY,WAAW,MAAM;AACxF,UAAI,qBAAM,2BAA2B,GAAG;AACtC,WAAO;AAAA,EACT;AAEA,QAAM,oBAAgB,gCAAkB,UAAU;AAClD,UAAI,qBAAM,aAAa,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,cAAc;AAC9B,QAAM,YAAY,KAAK,MAAM,QAAQ,gBAAgB,+BAA+B;AAEpF,MAAI,OAAO,MAAM,SAAS,GAAG;AAC3B,WAAO;AAAA,MACL,OAAO,IAAI;AAAA,QACT,sDAAsD,SAAS,qDAAqD,QAAQ,EAAE;AAAA,MAChI;AAAA,IACF;AAAA,EACF;AAEA,QAAM,cAAc,KAAK,IAAI;AAC7B,MAAI,KAAK,IAAI,cAAc,SAAS,IAAI,YAAY,KAAM;AACxD,WAAO;AAAA,MACL,OAAO,IAAI;AAAA,QACT,2GAA2G,SAAS;AAAA,MACtH;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,QAAQ;AACzB;AAEA,SAAS,uBACP,SACA,WACA,QACsD;AACtD,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,MACL,OAAO,IAAI,qDAAuC,kCAAkC;AAAA,IACtF;AAAA,EACF;AAEA,MAAI,UAAU,WAAW,GAAG;AAC1B,WAAO;AAAA,MACL,OAAO,IAAI;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,MACL,OAAO,IAAI;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,oBAAoB,OACvB,WAAW,UAAU,MAAM,EAC3B,OAAO,SAAS,MAAM,EACtB,OAAO,KAAK;AAEf,MAAI,cAAc,mBAAmB;AACnC,WAAO;AAAA,MACL,OAAO,IAAI,qDAAuC,oCAAoC;AAAA,IACxF;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,KAAK;AACtB;","names":[]}
1
+ {"version":3,"sources":["../../src/verify.ts"],"sourcesContent":["import * as crypto from \"node:crypto\";\nimport {\n type PlainWebhookError,\n PlainWebhookPayloadError,\n PlainWebhookSignatureVerificationError,\n} from \"./errors.js\";\nimport { parsePlainWebhook } from \"./parse.js\";\nimport { isErr, type Result } from \"./result.js\";\nimport type { WebhooksSchemaDefinition } from \"./webhook-schema.js\";\n\n/**\n * Verifies and validates a Plain webhook payload.\n *\n * @param payload - The raw request body.\n * @param signature - The value of the 'Plain-Request-Signature' header.\n * @param secret - Your Plain webhook signing secret.\n * @param tolerance - (Optional) Maximum allowed difference in seconds between the timestamp and the current time. Defaults to 300 seconds. This is helpful to prevent replay attacks.\n * @returns A Result object containing the validated payload or an error.\n */\nexport function verifyPlainWebhook(\n payloadRaw: string,\n signature: string,\n secret: string,\n tolerance = 300, // 5 minutes\n): Result<WebhooksSchemaDefinition, PlainWebhookError> {\n const signatureVerificationResult = verifyWebhookSignature(payloadRaw, signature, secret);\n if (isErr(signatureVerificationResult)) {\n return signatureVerificationResult;\n }\n\n const payloadResult = parsePlainWebhook(payloadRaw);\n if (isErr(payloadResult)) {\n return payloadResult;\n }\n\n const payload = payloadResult.data;\n const timestamp = Date.parse(payload.webhookMetadata.webhookDeliveryAttemptTimestamp);\n\n if (Number.isNaN(timestamp)) {\n return {\n error: new PlainWebhookPayloadError(\n `Invalid timestamp provided in the webhook payload: ${timestamp}. This is likely a bug in the Plain API (eventId: ${payload.id}).`,\n ),\n };\n }\n\n const currentTime = Date.now();\n if (Math.abs(currentTime - timestamp) > tolerance * 1000) {\n return {\n error: new PlainWebhookSignatureVerificationError(\n `The timestamp provided in the webhook payload is too far in the past. The maximum allowed difference is ${tolerance} seconds.`,\n ),\n };\n }\n\n return { data: payload };\n}\n\nfunction verifyWebhookSignature(\n payload: string,\n signature: string,\n secret: string,\n): Result<true, PlainWebhookSignatureVerificationError> {\n if (payload.length === 0) {\n return {\n error: new PlainWebhookSignatureVerificationError(\"No webhook payload was provided.\"),\n };\n }\n\n if (signature.length === 0) {\n return {\n error: new PlainWebhookSignatureVerificationError(\n 'No signature header value was provided. Please pass the value of the \"Plain-Request-Signature\" header.',\n ),\n };\n }\n\n if (secret.length === 0) {\n return {\n error: new PlainWebhookSignatureVerificationError(\n \"No webhook secret was provided. You can find your webhook secret in your workspace settings.\",\n ),\n };\n }\n\n const expectedSignature = crypto\n .createHmac(\"sha256\", secret)\n .update(payload, \"utf8\")\n .digest(\"hex\");\n\n // timingSafeEqual requires both buffers to be the same length; a length mismatch definitively\n // means the signature is wrong and leaks no secret. Compare the encoded byte lengths, not the\n // string lengths: the signature is caller-supplied, and a multi-byte character makes those two\n // disagree, which would get past a string-length check and then make timingSafeEqual throw.\n const given = Buffer.from(signature, \"utf8\");\n const expected = Buffer.from(expectedSignature, \"utf8\");\n\n if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {\n return {\n error: new PlainWebhookSignatureVerificationError(\"The signature provided is invalid.\"),\n };\n }\n\n return { data: true };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,aAAwB;AACxB,oBAIO;AACP,mBAAkC;AAClC,oBAAmC;AAY5B,SAAS,mBACd,YACA,WACA,QACA,YAAY,KACyC;AACrD,QAAM,8BAA8B,uBAAuB,YAAY,WAAW,MAAM;AACxF,UAAI,qBAAM,2BAA2B,GAAG;AACtC,WAAO;AAAA,EACT;AAEA,QAAM,oBAAgB,gCAAkB,UAAU;AAClD,UAAI,qBAAM,aAAa,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,cAAc;AAC9B,QAAM,YAAY,KAAK,MAAM,QAAQ,gBAAgB,+BAA+B;AAEpF,MAAI,OAAO,MAAM,SAAS,GAAG;AAC3B,WAAO;AAAA,MACL,OAAO,IAAI;AAAA,QACT,sDAAsD,SAAS,qDAAqD,QAAQ,EAAE;AAAA,MAChI;AAAA,IACF;AAAA,EACF;AAEA,QAAM,cAAc,KAAK,IAAI;AAC7B,MAAI,KAAK,IAAI,cAAc,SAAS,IAAI,YAAY,KAAM;AACxD,WAAO;AAAA,MACL,OAAO,IAAI;AAAA,QACT,2GAA2G,SAAS;AAAA,MACtH;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,QAAQ;AACzB;AAEA,SAAS,uBACP,SACA,WACA,QACsD;AACtD,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,MACL,OAAO,IAAI,qDAAuC,kCAAkC;AAAA,IACtF;AAAA,EACF;AAEA,MAAI,UAAU,WAAW,GAAG;AAC1B,WAAO;AAAA,MACL,OAAO,IAAI;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,MACL,OAAO,IAAI;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,oBAAoB,OACvB,WAAW,UAAU,MAAM,EAC3B,OAAO,SAAS,MAAM,EACtB,OAAO,KAAK;AAMf,QAAM,QAAQ,OAAO,KAAK,WAAW,MAAM;AAC3C,QAAM,WAAW,OAAO,KAAK,mBAAmB,MAAM;AAEtD,MAAI,MAAM,WAAW,SAAS,UAAU,CAAC,OAAO,gBAAgB,OAAO,QAAQ,GAAG;AAChF,WAAO;AAAA,MACL,OAAO,IAAI,qDAAuC,oCAAoC;AAAA,IACxF;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,KAAK;AACtB;","names":[]}
package/dist/verify.js CHANGED
@@ -55,7 +55,13 @@ function verifyWebhookSignature(payload, signature, secret) {
55
55
  .createHmac("sha256", secret)
56
56
  .update(payload, "utf8")
57
57
  .digest("hex");
58
- if (signature !== expectedSignature) {
58
+ // timingSafeEqual requires both buffers to be the same length; a length mismatch definitively
59
+ // means the signature is wrong and leaks no secret. Compare the encoded byte lengths, not the
60
+ // string lengths: the signature is caller-supplied, and a multi-byte character makes those two
61
+ // disagree, which would get past a string-length check and then make timingSafeEqual throw.
62
+ const given = Buffer.from(signature, "utf8");
63
+ const expected = Buffer.from(expectedSignature, "utf8");
64
+ if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {
59
65
  return {
60
66
  error: new PlainWebhookSignatureVerificationError("The signature provided is invalid."),
61
67
  };
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,EAEL,wBAAwB,EACxB,sCAAsC,GACvC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAe,MAAM,aAAa,CAAC;AAGjD;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,UAAkB,EAClB,SAAiB,EACjB,MAAc,EACd,SAAS,GAAG,GAAG;IAEf,MAAM,2BAA2B,GAAG,sBAAsB,CAAC,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IAC1F,IAAI,KAAK,CAAC,2BAA2B,CAAC,EAAE,CAAC;QACvC,OAAO,2BAA2B,CAAC;IACrC,CAAC;IAED,MAAM,aAAa,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;IACpD,IAAI,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;QACzB,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC;IACnC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,+BAA+B,CAAC,CAAC;IAEtF,IAAI,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,KAAK,EAAE,IAAI,wBAAwB,CACjC,sDAAsD,SAAS,qDAAqD,OAAO,CAAC,EAAE,IAAI,CACnI;SACF,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC/B,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,SAAS,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,CAAC;QACzD,OAAO;YACL,KAAK,EAAE,IAAI,sCAAsC,CAC/C,2GAA2G,SAAS,WAAW,CAChI;SACF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAED,SAAS,sBAAsB,CAC7B,OAAe,EACf,SAAiB,EACjB,MAAc;IAEd,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,KAAK,EAAE,IAAI,sCAAsC,CAAC,kCAAkC,CAAC;SACtF,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO;YACL,KAAK,EAAE,IAAI,sCAAsC,CAC/C,wGAAwG,CACzG;SACF,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,KAAK,EAAE,IAAI,sCAAsC,CAC/C,8FAA8F,CAC/F;SACF,CAAC;IACJ,CAAC;IAED,MAAM,iBAAiB,GAAG,MAAM;SAC7B,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;SAC5B,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC;SACvB,MAAM,CAAC,KAAK,CAAC,CAAC;IAEjB,IAAI,SAAS,KAAK,iBAAiB,EAAE,CAAC;QACpC,OAAO;YACL,KAAK,EAAE,IAAI,sCAAsC,CAAC,oCAAoC,CAAC;SACxF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACxB,CAAC"}
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-plain/webhooks",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Webhook parsing and signature verification for Plain webhooks",
5
5
  "license": "MIT",
6
6
  "author": "Plain <hello@plain.com> (https://plain.com)",