UNPKG

@portone/server-sdk

Version:

PortOne JavaScript SDK for server-side usage

155 lines (154 loc) 6.51 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _exportNames = { InvalidInputError: true, WebhookVerificationError: true, verify: true }; exports.WebhookVerificationError = exports.InvalidInputError = void 0; exports.verify = verify; var _webhook = require("./generated/webhook/index.cjs"); Object.keys(_webhook).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; if (key in exports && exports[key] === _webhook[key]) return; Object.defineProperty(exports, key, { enumerable: true, get: function () { return _webhook[key]; } }); }); var _PortOneError = require("./PortOneError.cjs"); var _timingSafeEqual = require("./utils/timingSafeEqual.cjs"); var _try = require("./utils/try.cjs"); const WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60; class InvalidInputError extends _PortOneError.PortOneError { /** @ignore */ constructor(message) { super(message); Object.setPrototypeOf(this, InvalidInputError.prototype); this.name = "InvalidInputError"; } } exports.InvalidInputError = InvalidInputError; class WebhookVerificationError extends _PortOneError.PortOneError { /** * 웹훅 검증이 실패한 상세 사유을 나타냅니다. */ reason; /** * 웹훅 검증 실패 사유로부터 에러 메시지를 생성합니다. * * @param reason 에러 메시지를 생성할 실패 사유 * @returns 에러 메시지 */ static getMessage(reason) { switch (reason) { case "MISSING_REQUIRED_HEADERS": return "\uD544\uC218 \uD5E4\uB354\uAC00 \uB204\uB77D\uB418\uC5C8\uC2B5\uB2C8\uB2E4."; case "NO_MATCHING_SIGNATURE": return "\uC62C\uBC14\uB978 \uC6F9\uD6C5 \uC2DC\uADF8\uB2C8\uCC98\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."; case "INVALID_SIGNATURE": return "\uC6F9\uD6C5 \uC2DC\uADF8\uB2C8\uCC98\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."; case "TIMESTAMP_TOO_OLD": return "\uC6F9\uD6C5 \uC2DC\uADF8\uB2C8\uCC98\uC758 \uD0C0\uC784\uC2A4\uD0EC\uD504\uAC00 \uB9CC\uB8CC \uAE30\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4."; case "TIMESTAMP_TOO_NEW": return "\uC6F9\uD6C5 \uC2DC\uADF8\uB2C8\uCC98\uC758 \uD0C0\uC784\uC2A4\uD0EC\uD504\uAC00 \uBBF8\uB798 \uC2DC\uAC04\uC73C\uB85C \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4."; } } /** @ignore */ constructor(reason, options) { super(WebhookVerificationError.getMessage(reason), options); Object.setPrototypeOf(this, WebhookVerificationError.prototype); this.name = "WebhookVerificationError"; this.reason = reason; } } exports.WebhookVerificationError = WebhookVerificationError; const prefix = "whsec_"; function findHeaderValue(headers, name) { if (typeof headers !== "object" || headers === null) return void 0; const nameLowerCase = name.toLowerCase(); let found = void 0; for (const [key, value] of Object.entries(headers)) { if (key.toLowerCase() === nameLowerCase) { for (const v of Array.isArray(value) ? value : [value]) { if (v == null) continue; if (typeof v !== "string") return void 0; if (found !== void 0) return void 0; found = v; } } } return found; } async function verify(secret, payload, headers) { if (typeof payload !== "string") throw new InvalidInputError("`payload` \uD30C\uB77C\uBBF8\uD130\uC758 \uD0C0\uC785\uC774 string\uC774 \uC544\uB2D9\uB2C8\uB2E4."); const msgId = findHeaderValue(headers, "webhook-id"); const msgSignature = findHeaderValue(headers, "webhook-signature"); const msgTimestamp = findHeaderValue(headers, "webhook-timestamp"); if (!msgId || !msgSignature || !msgTimestamp) { throw new WebhookVerificationError("MISSING_REQUIRED_HEADERS"); } verifyTimestamp(msgTimestamp); const expectedSignature = await sign(secret, msgId, msgTimestamp, payload); for (const versionedSignature of msgSignature.split(" ")) { const split = versionedSignature.split(",", 3); if (split.length < 2) continue; const [version, signature] = split; if (version !== "v1") continue; const signatureDecoded = (0, _try.tryCatch)(() => Uint8Array.from(atob(signature), c => c.charCodeAt(0)), () => void 0); if (signatureDecoded === void 0) continue; if ((0, _timingSafeEqual.timingSafeEqual)(signatureDecoded, expectedSignature)) { return JSON.parse(payload); } } throw new WebhookVerificationError("NO_MATCHING_SIGNATURE"); } async function sign(secret, msgId, msgTimestamp, payload) { const cryptoKey = await getCryptoKeyFromSecret(secret); const encoder = new TextEncoder(); const toSign = encoder.encode(`${msgId}.${msgTimestamp}.${payload}`); return await crypto.subtle.sign("HMAC", cryptoKey, toSign); } const secrets = /* @__PURE__ */new Map(); async function getCryptoKeyFromSecret(secret) { const cryptoKeyCached = secrets.get(secret); if (cryptoKeyCached !== void 0) return cryptoKeyCached; let rawSecret; if (secret instanceof Uint8Array) { rawSecret = secret; } else if (typeof secret === "string") { const secretBase64 = secret.startsWith(prefix) ? secret.substring(prefix.length) : secret; rawSecret = (0, _try.tryCatch)(() => Uint8Array.from(atob(secretBase64), c => c.charCodeAt(0)), () => { throw new InvalidInputError("`secret` \uD30C\uB77C\uBBF8\uD130\uAC00 \uC62C\uBC14\uB978 Base64 \uBB38\uC790\uC5F4\uC774 \uC544\uB2D9\uB2C8\uB2E4."); }); } else { throw new InvalidInputError("`secret` \uD30C\uB77C\uBBF8\uD130\uC758 \uD0C0\uC785\uC774 \uC798\uBABB\uB418\uC5C8\uC2B5\uB2C8\uB2E4."); } if (rawSecret.length === 0) { throw new InvalidInputError("\uC2DC\uD06C\uB9BF\uC740 \uBE44\uC5B4 \uC788\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."); } const cryptoKey = await crypto.subtle.importKey("raw", rawSecret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); secrets.set(secret, cryptoKey); return cryptoKey; } function verifyTimestamp(timestampHeader) { const now = Math.floor(Date.now() / 1e3); const timestamp = Number.parseInt(timestampHeader, 10); if (Number.isNaN(timestamp)) { throw new WebhookVerificationError("INVALID_SIGNATURE"); } if (now - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) { throw new WebhookVerificationError("TIMESTAMP_TOO_OLD"); } if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) { throw new WebhookVerificationError("TIMESTAMP_TOO_NEW"); } }