Home

Mangrove Reviews JS Library

Read and write Open Reviews from JavaScript. The library covers the reviewer API, signing with a key the app holds, logging people in through the signer so the app holds no keys, binding a key to a reviewer identity, and following a subject or reviewer from the Fediverse.

npm

Installation

npm install mangrove-reviews

The library needs fetch, URL and WebCrypto, which browsers and Node 20 and later provide. A script tag can load dist/index.umd.js instead; it defines mangrove-reviews, with the signer calls under signer.

Read reviews

import { getReviews, getSubject } from 'mangrove-reviews'

const reviews = await getReviews({ sub: 'https://example.com' })
const aggregate = await getSubject('https://example.com')

Write reviews with a local key

The app generates a keypair and keeps it, for example as JWK in IndexedDB. Write test reviews of the test subjects, such as https://example.com. Listings leave those reviews out unless the query sets examples: true.

import {
  generateKeypair,
  keypairToJwk,
  signAndSubmitReview
} from 'mangrove-reviews'

const keypair = await generateKeypair()
const jwk = await keypairToJwk(keypair) // store this to keep the same reviewer

await signAndSubmitReview(keypair, {
  sub: 'https://example.com',
  rating: 75,
  opinion: 'Great website to be used as an example.',
  metadata: { nickname: 'docs reader' }
})

Write reviews through the signer

The signer holds one key per person who logs in with OpenStreetMap, Bluesky, Google, GitHub or a passkey, so the app manages no keys and the person keeps one identity across devices and apps. The app needs a client id; the login guide says how to get one and walks through the callback.

import {
  loginUrl,
  parseCallback,
  signAndSubmitReview
} from 'mangrove-reviews/signer'

window.location.href = loginUrl(clientId, callbackUrl, 'osm', undefined, state)

// On the callback page:
const { sessionToken, did } = parseCallback(window.location.href)
await signAndSubmitReview(sessionToken, {
  sub: 'https://example.com',
  rating: 80
})

An app with an OpenStreetMap login of its own exchanges that login for a session, so the person logs in once. The app adds the openid scope to its OpenStreetMap login, and the signer operator registers the app's OpenStreetMap client id:

import { exchangeOsmIdToken } from 'mangrove-reviews/signer'

// tokens is the response of the app's own OpenStreetMap token request.
const { sessionToken, did } = await exchangeOsmIdToken(
  clientId,
  tokens.id_token
)

The person manages passkeys, keys, aliases and the recovery key on the signer's account page, where they log in again. An app links to it, and may propose its local key to list:

import { manageUrl } from 'mangrove-reviews/signer'
import { didKeyFromPem, publicToPem } from 'mangrove-reviews'

const pem = await publicToPem(localKeypair.publicKey)
window.location.href = manageUrl({
  addKey: didKeyFromPem(pem),
  returnTo: callbackUrl
})

Each operation of an identity carries its own signature, so any app publishes the identity to the PLC directory with one call:

import { publishIdentity } from 'mangrove-reviews'

const { url } = await publishIdentity(did)

Follow from the Fediverse

import { getFediverseHandle, followUrl } from 'mangrove-reviews'

const { acct, actor } = await getFediverseHandle({ sub: 'https://example.com' })
// Show `acct` with a copy button, or open the follow dialog on the person's server:
window.open(followUrl('mastodon.social', actor))

Name the app that wrote a review

The Open Reviews Association publishes the apps that write reviews at https://open-reviews.net/apps.json. appForClientId finds the app for a review's metadata.client_id, and gives the host of the client_id for an app missing from the list. An app served from localhost or another loopback host gives null.

import { getApps, appForClientId } from 'mangrove-reviews'

const apps = await getApps()
const app = appForClientId(review.payload.metadata?.client_id, apps)
// app is { name: 'MapComplete', url: 'https://mapcomplete.org/' }, { name: host }, or null

getClientCounts gives the number of reviews of subjects that each client wrote, keyed by client_id without its query string and fragment. An edited review counts once.

import { getApps, appForClientId, getClientCounts } from 'mangrove-reviews'

const apps = await getApps()
const since = Math.floor(Date.now() / 1000) - 90 * 86400
const perApp = {}
for (const [clientId, count] of Object.entries(
  await getClientCounts({ gt_iat: since })
)) {
  const app = appForClientId(clientId, apps)
  if (app) perApp[app.name] = (perApp[app.name] || 0) + count
}

Errors

A refused request rejects with an ApiError carrying the HTTP status and the server's reason in body, such as the field of a review that is wrong or 401 for an expired session.

License

Apache-2.0

index.js

signer.js

Log reviewers in with an account they already have and sign reviews through the signer, which holds one key per person.

Source: