@paramira/auth-sdk
v0.1.0
Published
Verify a visitor controls a paramira community identity (did:iwacu:...) without your app ever touching their private key.
Maintainers
Readme
@paramira/auth-sdk
Lets a third-party ("relying party") app verify that its visitor controls
a paramira community identity (did:iwacu:...) — without that app ever
touching the identity's private key. The key stays in whichever paramira
app the person already trusts (abantu, isoko, iwacu); your app just asks
that app to vouch for them.
Once you have a verified DID, you can gate features by whatever community protocol the person has adopted — see Checking a feature below. No sign-in is needed for that part; the adopted-protocols endpoint is public by design.
A minimal working example — "Sign in with abantu" gating a feature in a
small community app — is in demo/.
How it works
- Your app creates a sign-in challenge and registers it with paramira's identity directory.
- You show a QR code (desktop) or deep-link button (mobile) built from that challenge.
- The person scans/opens it in their paramira app (abantu, isoko, or iwacu), which shows them what's asking and lets them approve.
- On approval, that app signs a proof and posts it back to the directory.
- Your app, which has been polling, picks up the signed result and verifies it itself — it never just trusts the directory's "ok".
This mirrors the existing "proposal-requests" pattern already used elsewhere in this codebase (queue a request, the identity-holding app picks it up and fulfills it through its own signed path), applied to authentication instead of governance.
This package is deliberately UI-free — no QR rendering, no framework
dependency. Pair it with whatever QR library you already use (the demo
uses qrcode).
Install
npm install @paramira/auth-sdkRelying-party integration
import {
createSignInChallenge,
registerSignInRequest,
buildSignInLink,
waitForSignIn,
consumeSignInRequest,
} from '@paramira/auth-sdk'
const DIRECTORY_URL = 'https://iwacurelay.paramira.org/directory'
const DEVICE_APP_URL = 'https://abantu.paramira.org/signin' // wherever your users' identity app is hosted
async function startSignIn() {
const { requestId, nonce } = createSignInChallenge()
const origin = window.location.origin
await registerSignInRequest({
directoryUrl: DIRECTORY_URL,
requestId,
nonce,
origin,
label: 'My Community App', // shown to the user in their approval prompt
})
const link = buildSignInLink(DEVICE_APP_URL, requestId)
// Render `link` as a QR code (e.g. with the `qrcode` package), or as a
// deep-link button on mobile where the identity app might be installed.
showQrCode(link)
const did = await waitForSignIn({ directoryUrl: DIRECTORY_URL, requestId, origin, nonce })
await consumeSignInRequest({ directoryUrl: DIRECTORY_URL, requestId })
if (!did) {
showError('Sign-in timed out or was not approved.')
return
}
// `did` is now a verified identity — start a session however your app
// normally does (e.g. store it server-side against a cookie session).
onSignedIn(did)
}waitForSignIn polls for you and verifies the result before returning —
use pollSignInRequest directly instead if you need your own polling loop
or cancellation logic (it takes an AbortSignal too).
Checking a feature
Once you have a verified did, checking whether they've adopted a
community protocol needs no further sign-in — it's a plain public GET:
const res = await fetch(`${DIRECTORY_URL}/api/identity/protocols?did=${encodeURIComponent(did)}`)
const { protocolIds } = await res.json()
const hasFamilyHistory = protocolIds.includes('family-history:v1')For anything beyond a plain includes() check (protocol composition via
requires, community-defined protocols, param overrides), use
@paramira/protocol-core's computeFeatures/createProtocolClient
against the same catalog every paramira app already shares.
Device-app integration (only needed if you're building an identity-holding app)
Abantu already implements this (see apps/abantu/app/composables/useSignIn.ts
and apps/abantu/app/pages/signin.vue) — most integrators only need the
relying-party half above. If you're building another app that holds
community identities:
import { parseSignInHash, fetchSignInRequest, fulfillSignInRequest } from '@paramira/auth-sdk'
// On your app's sign-in landing page:
const parsed = parseSignInHash(window.location.hash)
if (!parsed) {
showInvalidLink()
} else {
const request = await fetchSignInRequest({ directoryUrl: DIRECTORY_URL, requestId: parsed.requestId })
if (!request || request.status !== 'pending') {
showExpiredOrAlreadyUsed()
} else {
showApprovalPrompt(request.label ?? request.origin, async () => {
await fulfillSignInRequest({
directoryUrl: DIRECTORY_URL,
requestId: parsed.requestId,
origin: request.origin,
nonce: request.nonce,
did: myIdentity.did,
sign: myIdentity.sign, // (payload: string) => Promise<string>
})
showApproved()
})
}
}Security notes
- A signed fulfillment is only accepted within 5 minutes of being signed —
see
MAX_FULFILLMENT_AGE_MSinsignin.js. - The payload the device signs binds
requestId,origin, andnoncetogether, so a signature can't be replayed against a different relying party or a different pending request. - Always call
verifySignInFulfillment(done for you insidewaitForSignIn) against the challenge you generated — never trust a directory response's echoedorigin/nonceas the source of truth. - The identity directory (
services/directory) is the only relay this package talks to today. The protocol is simple enough to self-host a compatible relay later if that becomes necessary.
License
MIT
