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
})
A logged-in person can add a passkey to their account, list their passkeys and remove one:
import {
passkeyAddUrl,
listPasskeys,
removePasskey
} from 'mangrove-reviews/signer'
window.location.href = await passkeyAddUrl(sessionToken, callbackUrl, state)
// The signer's page returns to callbackUrl with #passkey=added&state=... in the fragment.
const passkeys = await listPasskeys(sessionToken)
await removePasskey(sessionToken, passkeys[0].id)
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