@portone/server-sdk
Version:
PortOne JavaScript SDK for server-side usage
155 lines (154 loc) • 6.51 kB
JavaScript
;
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");
}
}