dragoman 0.4.71

Server for scholarly metadata in commonmeta format stored in a SQLite database.
// A bookmark link carries the bookmarked PIDs, not their metadata: the receiving
// dragoman looks each one up in its own store. The ids are joined by newlines,
// deflated and base64url-encoded, which roughly halves a list of DOI, ORCID and
// ROR URLs — their shared resolver prefixes are what deflate removes — and keeps
// the link short enough for a QR code a phone camera can read.

export const BOOKMARKS_PARAM = 'bookmarks'

async function pipe(bytes, stream) {
  const out = new Blob([bytes]).stream().pipeThrough(stream)
  return new Uint8Array(await new Response(out).arrayBuffer())
}

function toBase64url(bytes) {
  let bin = ''
  for (const b of bytes) bin += String.fromCharCode(b)
  return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}

function fromBase64url(s) {
  const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/'))
  return Uint8Array.from(bin, c => c.charCodeAt(0))
}

export async function encodeBookmarkIds(ids) {
  const bytes = new TextEncoder().encode(ids.join('\n'))
  return toBase64url(await pipe(bytes, new CompressionStream('deflate-raw')))
}

// The ids a link carries, or null when the parameter is not one this encoder
// wrote. Only PID URLs survive, so a hand-edited link cannot smuggle in
// anything the bookmark list would not hold.
export async function decodeBookmarkIds(param) {
  try {
    const bytes = await pipe(fromBase64url(param), new DecompressionStream('deflate-raw'))
    const ids = new TextDecoder().decode(bytes).split('\n').filter(id => /^https?:\/\/\S+$/.test(id))
    return ids.length > 0 ? [...new Set(ids)] : null
  } catch {
    return null
  }
}

export async function bookmarkLink(ids, origin) {
  const url = new URL('/', origin)
  url.searchParams.set(BOOKMARKS_PARAM, await encodeBookmarkIds(ids))
  return url.href
}