// src/client.ts
var DEFAULT_BASE_URL = "https://horamatch.com/api/v1";
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
var HoraMatchApiError = class extends Error {
  constructor(code, status, requestId, message, details) {
    super(message);
    this.code = code;
    this.status = status;
    this.requestId = requestId;
    this.details = details;
    this.name = "HoraMatchApiError";
  }
  code;
  status;
  requestId;
  details;
};
var HoraMatchNetworkError = class extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = "HoraMatchNetworkError";
  }
};
var HoraMatchTimeoutError = class extends HoraMatchNetworkError {
  constructor(options) {
    super("HoraMatch request timed out.", options);
    this.name = "HoraMatchTimeoutError";
  }
};
var HoraMatch = class {
  eventTypes = {
    list: () => this.request("GET", "/event-types"),
    retrieve: (id) => this.request("GET", `/event-types/${encodeURIComponent(id)}`)
  };
  availability = {
    list: (eventTypeId, input) => this.request("GET", `/event-types/${encodeURIComponent(eventTypeId)}/slots?${new URLSearchParams(input)}`)
  };
  bookings = {
    list: (input = {}) => this.request("GET", `/bookings?${queryString(input)}`),
    retrieve: (uid) => this.request("GET", `/bookings/${encodeURIComponent(uid)}`),
    create: (input, options) => this.request("POST", "/bookings", input, options.idempotencyKey),
    cancel: (uid, input, options) => this.request("POST", `/bookings/${encodeURIComponent(uid)}/cancel`, input, options.idempotencyKey)
  };
  webhooks = {
    list: () => this.request("GET", "/webhooks/endpoints"),
    create: (input, options) => this.request("POST", "/webhooks/endpoints", input, options.idempotencyKey),
    remove: (id, options) => this.request("DELETE", `/webhooks/endpoints/${encodeURIComponent(id)}`, void 0, options.idempotencyKey)
  };
  apiKey;
  baseUrl;
  fetcher;
  timeoutMs;
  maxRetries;
  constructor(options) {
    if (!options.apiKey || !options.apiKey.startsWith("hm_live_")) throw new TypeError("A HoraMatch API key is required.");
    this.apiKey = options.apiKey;
    this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/u, "");
    this.fetcher = options.fetch ?? globalThis.fetch;
    if (!this.fetcher) throw new TypeError("A fetch implementation is required.");
    this.timeoutMs = options.timeoutMs ?? 1e4;
    this.maxRetries = options.maxRetries ?? 2;
  }
  async request(method, path, body, idempotencyKey) {
    if (method !== "GET" && (!idempotencyKey || idempotencyKey.length < 16)) throw new TypeError("Mutations require an Idempotency-Key of at least 16 characters.");
    const serializedBody = body === void 0 ? void 0 : JSON.stringify(body);
    const retryableMethod = method === "GET" || Boolean(idempotencyKey);
    for (let attempt = 0; ; attempt += 1) {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(new DOMException("Timeout", "TimeoutError")), this.timeoutMs);
      try {
        const response = await this.fetcher(`${this.baseUrl}${path}`, {
          method,
          signal: controller.signal,
          headers: {
            authorization: `Bearer ${this.apiKey}`,
            accept: "application/json",
            ...serializedBody ? { "content-type": "application/json" } : {},
            ...idempotencyKey ? { "idempotency-key": idempotencyKey } : {}
          },
          body: serializedBody
        });
        if (response.ok) return await response.json();
        if (retryableMethod && attempt < this.maxRetries && RETRYABLE_STATUS.has(response.status)) {
          await delay(retryDelay(response, attempt));
          continue;
        }
        throw await apiError(response);
      } catch (error) {
        if (error instanceof HoraMatchApiError) throw error;
        if (controller.signal.aborted) throw new HoraMatchTimeoutError({ cause: error });
        if (retryableMethod && attempt < this.maxRetries) {
          await delay(backoff(attempt));
          continue;
        }
        throw new HoraMatchNetworkError("HoraMatch request failed.", { cause: error });
      } finally {
        clearTimeout(timeout);
      }
    }
  }
};
async function apiError(response) {
  let payload = null;
  try {
    payload = await response.json();
  } catch {
  }
  const error = payload && typeof payload === "object" ? Reflect.get(payload, "error") : null;
  const code = error && typeof error === "object" && typeof Reflect.get(error, "code") === "string" ? String(Reflect.get(error, "code")) : `HTTP_${response.status}`;
  const message = error && typeof error === "object" && typeof Reflect.get(error, "message") === "string" ? String(Reflect.get(error, "message")) : "HoraMatch API request failed.";
  const requestId = error && typeof error === "object" && typeof Reflect.get(error, "requestId") === "string" ? String(Reflect.get(error, "requestId")) : response.headers.get("x-request-id");
  const details = error && typeof error === "object" ? Reflect.get(error, "details") : void 0;
  return new HoraMatchApiError(code, response.status, requestId, message, details);
}
function queryString(input) {
  const query = new URLSearchParams();
  for (const [key, value] of Object.entries(input)) if (value !== void 0) query.set(key, String(value));
  return query.toString();
}
function retryDelay(response, attempt) {
  const raw = response.headers.get("retry-after");
  const seconds = raw === null ? Number.NaN : Number(raw);
  return Number.isFinite(seconds) && seconds >= 0 ? Math.min(seconds * 1e3, 6e4) : backoff(attempt);
}
function backoff(attempt) {
  return Math.floor(Math.random() * Math.min(2e3, 200 * 2 ** attempt));
}
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// src/webhooks.ts
async function verifyWebhook(input) {
  const headers = input.headers instanceof Headers ? input.headers : new Headers(input.headers);
  const eventId = headers.get("x-horamatch-id") ?? "";
  const timestamp = headers.get("x-horamatch-timestamp") ?? "";
  const signature = headers.get("x-horamatch-signature") ?? "";
  if (!eventId || !/^\d{10}$/u.test(timestamp) || !signature.startsWith("v1=")) throw new Error("Invalid HoraMatch webhook headers.");
  const tolerance = input.toleranceSeconds ?? 300;
  const nowSeconds = Math.floor((input.now ?? Date.now()) / 1e3);
  if (Math.abs(nowSeconds - Number(timestamp)) > tolerance) throw new Error("HoraMatch webhook timestamp is outside the tolerance window.");
  const expected = await hmac(input.secret, `${eventId}.${timestamp}.${input.payload}`);
  if (!constantTimeHexEqual(signature.slice(3), expected)) throw new Error("Invalid HoraMatch webhook signature.");
  const event = JSON.parse(input.payload);
  if (!event || typeof event !== "object" || Reflect.get(event, "id") !== eventId) throw new Error("Invalid HoraMatch webhook payload.");
  return event;
}
async function hmac(secret, message) {
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const digest = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
}
function constantTimeHexEqual(left, right) {
  if (!/^[a-f0-9]{64}$/u.test(left) || left.length !== right.length) return false;
  let mismatch = 0;
  for (let index = 0; index < left.length; index += 1) mismatch |= left.charCodeAt(index) ^ right.charCodeAt(index);
  return mismatch === 0;
}

export { HoraMatch, HoraMatchApiError, HoraMatchNetworkError, HoraMatchTimeoutError, verifyWebhook };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map