@linear/sdk
Version:
The Linear Client SDK for interacting with the Linear GraphQL API
286 lines (283 loc) • 10.7 kB
JavaScript
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __export = (all, symbols) => {
let target = {};
for (var name in all) {
__defProp(target, name, {
get: all[name],
enumerable: true
});
}
if (symbols) {
__defProp(target, Symbol.toStringTag, { value: "Module" });
}
return target;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) {
__defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
}
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let crypto = require("crypto");
crypto = __toESM(crypto);
//#region src/webhooks/client.ts
const LINEAR_WEBHOOK_SIGNATURE_HEADER = "linear-signature";
const LINEAR_WEBHOOK_TS_HEADER = "linear-timestamp";
const LINEAR_WEBHOOK_TS_FIELD = "webhookTimestamp";
/**
* Client for handling Linear webhook requests with helpers.
*/
var LinearWebhookClient = class {
/**
* Creates a new LinearWebhookClient instance
* @param secret The webhook signing secret. See https://linear.app/developers/webhooks#securing-webhooks.
*/
constructor(secret) {
this.secret = secret;
if (typeof secret !== "string" || secret.length === 0) throw new TypeError("LinearWebhookClient: secret must be a non-empty string");
}
/**
* Creates a webhook handler function that can process Linear webhook requests
* @returns A webhook handler function with event registration capabilities.
* Supports both Fetch API `(request: Request) => Promise<Response>` and
* Node.js `(request: IncomingMessage, response: ServerResponse) => Promise<void>`
*/
createHandler() {
const eventHandlers = /* @__PURE__ */ new Map();
const handler = async (requestOrMessage, response) => {
const adapter = this.getHttpAdapter(requestOrMessage, response);
try {
if (adapter.method !== "POST") return adapter.send(405, "Method not allowed");
const signature = adapter.signature;
if (!signature) return adapter.send(400, "Missing webhook signature");
const rawBody = await adapter.readRawBody();
let parsedPayload;
try {
parsedPayload = this.parseVerifiedPayload(rawBody, signature, adapter.timestamp);
} catch {
return adapter.send(400, "Invalid webhook");
}
const allHandlers = this.collectHandlers(eventHandlers, parsedPayload.type);
await Promise.all(allHandlers.map((h) => h(parsedPayload)));
return adapter.send(200, "OK");
} catch {
return adapter.send(500, "Internal server error");
}
};
handler.on = function(eventType, eventHandler) {
const handlers = eventHandlers.get(eventType) || [];
handlers.push(eventHandler);
eventHandlers.set(eventType, handlers);
};
handler.off = function(eventType, eventHandler) {
const handlers = eventHandlers.get(eventType);
if (handlers) {
const index = handlers.indexOf(eventHandler);
if (index > -1) {
handlers.splice(index, 1);
if (handlers.length === 0) eventHandlers.delete(eventType);
}
}
};
handler.removeAllListeners = function(eventType) {
if (eventType) eventHandlers.delete(eventType);
else eventHandlers.clear();
};
return handler;
}
/**
* Determines whether the provided value is a Fetch API `Request`.
* Used as a type guard to select the appropriate runtime path.
*
* @param value - Unknown request-like value
* @returns True if `value` is a Fetch API `Request`
*/
isFetchRequest(value) {
return typeof value === "object" && value !== null && "arrayBuffer" in value && typeof Reflect.get(value, "arrayBuffer") === "function";
}
/**
* Creates an HTTP adapter for Fetch-based runtimes.
* The body is not read until `readRawBody` is invoked.
*
* @param request - Fetch API `Request`
* @returns Helpers to read input and send responses in a unified way
*/
createFetchAdapter(request) {
return {
method: request.method,
signature: request.headers.get(LINEAR_WEBHOOK_SIGNATURE_HEADER),
timestamp: request.headers.get(LINEAR_WEBHOOK_TS_HEADER),
readRawBody: async () => Buffer.from(await request.arrayBuffer()),
send: (status, body) => new Response(body, { status })
};
}
/**
* Creates an HTTP adapter for Node.js HTTP runtimes.
* The body stream is consumed when `readRawBody` is invoked.
*
* @param incomingMessage - Node.js `IncomingMessage`
* @param res - Node.js `ServerResponse` used to write the response
* @returns Helpers to read input and send responses in a unified way
*/
createNodeAdapter(incomingMessage, res) {
const signatureHeader = incomingMessage.headers[LINEAR_WEBHOOK_SIGNATURE_HEADER];
const signature = Array.isArray(signatureHeader) ? signatureHeader[0] ?? null : signatureHeader ?? null;
const timestampHeader = incomingMessage.headers[LINEAR_WEBHOOK_TS_HEADER];
const timestamp = Array.isArray(timestampHeader) ? timestampHeader[0] ?? null : timestampHeader ?? null;
return {
method: incomingMessage.method || "",
signature,
timestamp,
readRawBody: async () => {
const chunks = [];
for await (const chunk of incomingMessage) chunks.push(Buffer.from(chunk));
return Buffer.concat(chunks);
},
send: (status, body) => {
res.statusCode = status;
res.end(body);
}
};
}
/**
* Selects and constructs the appropriate HTTP adapter for the
* provided request type (Fetch or Node.js HTTP).
*
* @param requestOrMessage - A Fetch `Request` or Node.js `IncomingMessage`
* @param response - Node.js `ServerResponse` (required for Node path)
* @returns An HTTP adapter with unified IO helpers
*/
getHttpAdapter(requestOrMessage, response) {
return this.isFetchRequest(requestOrMessage) ? this.createFetchAdapter(requestOrMessage) : this.createNodeAdapter(requestOrMessage, response);
}
/**
* Parses the JSON body and verifies signature and optional timestamp.
*
* Throws if the JSON is invalid, the signature is invalid, or the timestamp check fails.
*
* @param rawBody - Raw request body as a Buffer
* @param signature - The value of the `linear-signature` header
* @param timestampHeader - The value of the `linear-timestamp` header (used only when the signed body has no timestamp)
* @returns The verified and parsed webhook payload
*/
parseVerifiedPayload(rawBody, signature, timestampHeader) {
const parsedBody = this.parseBodyAsWebhookPayload(rawBody);
const timestamp = parsedBody.webhookTimestamp ?? timestampHeader;
if (!this.verify(rawBody, signature, timestamp)) throw new Error("Invalid webhook signature");
return parsedBody;
}
/**
* Parses the raw body as a webhook payload with typing.
*
* @param rawBody - Raw request body as a Buffer
* @returns Parsed webhook payload object
*/
parseBodyAsWebhookPayload(rawBody) {
return JSON.parse(rawBody.toString());
}
/**
* Returns the list of handlers to invoke for a given event type,
* including both specific and wildcard handlers.
*
* @param eventHandlers - Internal registry of event handlers
* @param eventType - The webhook `type` field from the payload
* @returns Ordered list of handlers to be executed
*/
collectHandlers(eventHandlers, eventType) {
const specificHandlers = eventHandlers.get(eventType) || [];
const wildcardHandlers = eventHandlers.get("*") || [];
return [...specificHandlers, ...wildcardHandlers];
}
/**
* Verify the webhook signature
*
* Throws an error if the signature or timestamp is invalid.
*
* @param rawBody The webhook request raw body
* @param signature The signature to verify
* @param timestamp The timestamp value - either from the `linear-timestamp` header (string)
* or the `webhookTimestamp` field from the request parsed body (number)
* @returns True if the signature is valid
*/
verify(rawBody, signature, timestamp) {
const verificationBuffer = Buffer.from(crypto.default.createHmac("sha256", this.secret).update(rawBody).digest("hex"));
const signatureBuffer = Buffer.from(signature);
if (verificationBuffer.length !== signatureBuffer.length) throw new Error("Invalid webhook signature");
if (!crypto.default.timingSafeEqual(verificationBuffer, signatureBuffer)) throw new Error("Invalid webhook signature");
if (timestamp) {
const timestampMs = typeof timestamp === "string" ? parseInt(timestamp, 10) : timestamp;
if (isNaN(timestampMs)) throw new Error(`Invalid webhook timestamp: ${timestamp}`);
if (Math.abs((/* @__PURE__ */ new Date()).getTime() - timestampMs) > 1e3 * 60) throw new Error("Invalid webhook timestamp");
}
return true;
}
/**
* Parse and verify webhook data, throwing an error if the signature or given timestamp is invalid.
*
* @param rawBody The webhook request raw body
* @param signature The signature to verify
* @param timestamp The timestamp value - either from the `linear-timestamp` header (string)
* or the `webhookTimestamp` field from the request parsed body (number)
*/
parseData(rawBody, signature, timestamp) {
if (!this.verify(rawBody, signature, timestamp)) throw new Error("Invalid webhook signature");
return this.parseBodyAsWebhookPayload(rawBody);
}
};
//#endregion
Object.defineProperty(exports, 'LINEAR_WEBHOOK_SIGNATURE_HEADER', {
enumerable: true,
get: function () {
return LINEAR_WEBHOOK_SIGNATURE_HEADER;
}
});
Object.defineProperty(exports, 'LINEAR_WEBHOOK_TS_FIELD', {
enumerable: true,
get: function () {
return LINEAR_WEBHOOK_TS_FIELD;
}
});
Object.defineProperty(exports, 'LINEAR_WEBHOOK_TS_HEADER', {
enumerable: true,
get: function () {
return LINEAR_WEBHOOK_TS_HEADER;
}
});
Object.defineProperty(exports, 'LinearWebhookClient', {
enumerable: true,
get: function () {
return LinearWebhookClient;
}
});
Object.defineProperty(exports, '__commonJSMin', {
enumerable: true,
get: function () {
return __commonJSMin;
}
});
Object.defineProperty(exports, '__export', {
enumerable: true,
get: function () {
return __export;
}
});