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.
- Function reference for every call and its arguments.
- Integration guides for the flows: the login, identities and Fediverse.
- The servers behind the calls: reviewer API, signer and Fediverse bridge.
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