Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@noah/hey-email

HEY.com email integration with Kody: search threads, read messages/attachments, create drafts, and send with explicit confirmation.

src/client.js

90 lines · 2.6 KB · JavaScript
const BASE = 'https://app.hey.com'

/**
 * Resolve a request path against app.hey.com and refuse any other origin.
 * Attachment URLs come from untrusted email HTML, so a credentialed fetch must
 * never follow them off-site.
 *
 * @param {string} path
 * @returns {string}
 */
function resolveHeyUrl(path) {
  const raw = String(path || '')
  const url = raw.startsWith('http')
    ? new URL(raw)
    : new URL(`${BASE}${raw.startsWith('/') ? '' : '/'}${raw}`)
  if (url.origin !== BASE) {
    throw new Error(`Refusing to send HEY credentials to ${url.origin}`)
  }
  return url.toString()
}

/**
 * Authenticated fetch to app.hey.com using the heyToken secret.
 * @param {string} path
 * @param {RequestInit} [init]
 */
export async function heyFetch(path, init = {}) {
  const { data } = await heyRequest(path, init)
  return data
}

/**
 * Same as heyFetch but keeps status + response headers (Location on draft create).
 * @param {string} path
 * @param {RequestInit} [init]
 * @returns {Promise<{ status: number, headers: Headers, data: any }>}
 */
export async function heyRequest(path, init = {}) {
  const url = resolveHeyUrl(path)
  const headers = new Headers(init.headers || {})
  if (!headers.has('Authorization')) {
    headers.set('Authorization', 'Bearer {{secret:heyToken}}')
  }
  if (!headers.has('Accept')) headers.set('Accept', 'application/json')
  const res = await fetch(url, { ...init, headers })
  const text = await res.text()
  let data = text
  try {
    data = text ? JSON.parse(text) : null
  } catch {
    // keep text
  }
  if (!res.ok) {
    const err = new Error(`HEY ${res.status} ${path}`)
    err.status = res.status
    err.body = typeof data === 'string' ? data.slice(0, 500) : data
    throw err
  }
  return { status: res.status, headers: res.headers, data }
}

/**
 * Authenticated binary fetch (PDF attachments). Follows redirects.
 * @param {string} path
 * @param {RequestInit} [init]
 * @returns {Promise<Uint8Array>}
 */
export async function heyFetchBinary(path, init = {}) {
  const url = resolveHeyUrl(path)
  const headers = new Headers(init.headers || {})
  if (!headers.has('Authorization')) {
    headers.set('Authorization', 'Bearer {{secret:heyToken}}')
  }
  if (!headers.has('Accept')) headers.set('Accept', '*/*')
  const res = await fetch(url, { ...init, headers, redirect: 'follow' })
  if (!res.ok) {
    const err = new Error(`HEY binary ${res.status} ${path}`)
    err.status = res.status
    try {
      err.body = (await res.text()).slice(0, 500)
    } catch {
      err.body = null
    }
    throw err
  }
  const buf = await res.arrayBuffer()
  return new Uint8Array(buf)
}

export { BASE, resolveHeyUrl }