@blaaiz/signa-web-sdk
v3.0.0
Published
Signa and Signa ID for websites: open a KYC session, check if a wallet is verified, verify it, and ask for a person's verified data.
Readme
Signa web SDK
@blaaiz/signa-web-sdk adds Signa and Signa ID to a website. With it, a
site can:
- check if a wallet belongs to a verified Signa ID (
status), - let the person verify and link the wallet (
verify), - ask for the person's verified data, which the person approves
(
requestData), - open a business's Signa KYC session, where the person does the steps of
the session (
startSession).
verify, requestData and startSession show a page of the Signa ID site.
See "Dialog or popup".
Install
npm install @blaaiz/signa-web-sdkimport { createSigna } from "@blaaiz/signa-web-sdk";
const signa = createSigna({ environment: "production" });| environment | API | Signa ID site |
| ---------------------- | ----------------------------- | --------------------------- |
| production (default) | https://api-prod.blaaiz.com | https://id.blaaiz.com |
| sandbox | https://api-dev.blaaiz.com | https://id-dev.blaaiz.com |
createSigna uses no browser API, so a module that creates the client can
run during server rendering. Call status, verify, requestData and
startSession in the browser only.
The package has no dependencies. It ships ES module and CommonJS builds with TypeScript types, and a standalone build for a script tag.
Use a script tag
A page with no bundler loads the standalone build from a CDN. It sets
window.SignaWebSdk, which has the same createSigna:
<script src="https://cdn.jsdelivr.net/npm/@blaaiz/signa-web-sdk@3/dist/signa-web-sdk.min.js"></script>
<script>
const signa = SignaWebSdk.createSigna({ environment: "production" });
</script>unpkg.com/@blaaiz/signa-web-sdk@3 serves the same file. @3 gets every
3.x release. To add an integrity attribute, name an exact version, for
example @3.0.0: the file at @3 changes with each release.
Dialog or popup
verify(), requestData() and startSession() take a mode:
embedded(the default): a dialog on your page, with the Signa ID page in an iframe. The person does not leave your page. The browser does not block the dialog.popup: a separate window. Call the method from a click handler, or the browser blocks the window and the promise rejects withcode: 'blocked'.
In both modes, the person signs in to Signa ID with an email code each time, as in Sumsub ID. The Signa ID pages offer no passkey and keep no sign-in between calls.
Rules for both modes:
- Only one Signa dialog or popup can be open on the page. A second call for
the same wallet or token returns the same promise. Any other call rejects
with
code: 'in_progress'. - The SDK accepts a message only from its own dialog or popup, from the Signa ID site of its environment.
Rules for embedded:
- If your site sends a
Content-Security-Policywithframe-srcorchild-src, add the Signa ID site. - The dialog lets the iframe use the camera (
allow="camera") for the liveness check. If your site sends aPermissions-Policyheader, it must allow the camera for the Signa ID site, for examplecamera=(self "https://id.blaaiz.com"). The browser asks for the camera in the name of your site. - The person closes the dialog with its close button or Escape.
- Some wallet extensions do not run inside another site's frame. If the
person's wallet does not appear in the dialog, use
mode: 'popup'forverify().
Rules for popup:
- Do not send
Cross-Origin-Opener-Policy: same-originon the page. Usesame-origin-allow-popupsor no header. Withsame-origin, the popup loses its link to the page and cannot report back.
Check a wallet
A smart-contract exchange with no backend can use this too.
Call
signa.status()when the page loads. This call opens no window.Call
signa.verify()from the click handler of a button:await signa.status({ wallet, chainId: 8453 }); // on page load button.onclick = async () => { const status = await signa.verify({ wallet, chainId: 8453 }); if (status.verified) { // Let the person continue. } };
The types:
interface SignaStatus {
verified: boolean;
level: number | null;
country: string | null;
expires_at: string | null;
attestations: {
chain_id: number;
uid: string;
explorer_url: string | null;
}[];
}
interface SignaVerifyOptions {
wallet: string;
chainId?: number;
mode?: "embedded" | "popup"; // the default is 'embedded'
}status() takes the same options without mode.
How verify() works:
- The SDK reads
GET /api/v1/signa-id/public/wallets/<address>/status. This call needs no cookie. A verified wallet returns at once, with no dialog or window. - If the wallet is not verified, the SDK shows the connect page: the
dialog's iframe loads
/embed/connect?wallet=…&chain_id=…&origin=…, and a popup opens/connect?…with the same parameters. The person signs in with an email code. If necessary, the person verifies their identity in the same dialog or popup. Then the person links the wallet. - After the person links the wallet, or selects "Return to the site", the
page sends
{ type: 'signa:done' }to the SDK. The SDK removes the dialog or closes the popup. The message holds no personal data. - The SDK reads the status again and returns it.
Rules for integrators:
- Safari allows only a short time between the click and a popup. If
status()ran in the last 60 seconds and the wallet is not verified,verify()shows the page at once, before any request. - If the person closes the dialog or the popup, the promise resolves with the current status. That status is not verified.
- An address that is not
0xplus 40 hex characters rejects at once. status()keeps each result for 60 seconds. The API also caches it for 60 seconds.- If the status call fails, the promise rejects. Call again to retry.
Rules on the Signa ID side:
- The
originparameter must be an exact https origin, orhttp://localhostwith a port. Otherwise the page shows an error and sends nothing. - The page names the site only after the site proves its origin. The page
sends a nonce to
origin, and the SDK on that page sends it back. Until then, the page says "A site". - The page never reports on its own. It does not send a message until the person acts in it. Otherwise any site could test which wallets a person owns.
/connectrefuses to work inside a frame, and/embed/connectworks only inside one.- A self-check goes to the capture page in the same dialog or popup, then comes back to the connect page. The person stays signed in for this trip only.
- The page checks that the wallet extension uses the requested address and network. A different address stops the link with a message.
- "Return to the site" does not mean verified. The public status can still say not verified, for example when a document expired.
Ask for a person's data
A business can ask a Signa ID owner to release identity data to it. The
business's server creates the request with the Blaaiz API
(POST /api/external/signa-id/release-requests) and gets a request_token.
The business's page then calls signa.requestData().
button.onclick = async () => {
try {
const { releaseId, code } = await signa.requestData({ requestToken });
// Send releaseId and code to your server. Your server calls
// POST /api/external/signa-id/releases/exchange with { code } in 5 minutes.
} catch (error) {
// error.code is 'closed', 'blocked' or 'in_progress'.
}
};The types:
interface SignaReleaseOptions {
requestToken: string; // the request_token, 43 base64url characters
mode?: "embedded" | "popup"; // the default is 'embedded'
}
interface SignaRelease {
releaseId: string; // uuid
code: string; // one use, 5 minutes, 43 base64url characters
}How requestData() works:
- The SDK shows the release page at once, before any request. A popup
opens
/r/<request_token>?origin=<the page's origin>. The dialog's iframe loads/embed/release?origin=<the page's origin>, with no token in the URL. - The page sends
{ type: 'signa:hello', nonce }toorigin. The SDK answers{ type: 'signa:ready', nonce }. Inembeddedmode, the answer also carriesrequest_token. The page does nothing else until this answer comes. With no answer in 10 seconds, it shows "This link does not work". - The page calls
POST /api/v1/kyc-capture/signa-release/sessionwith{token, origin}. The service comparesoriginwith the origin that the business gave at create. A different origin, or a bad or expired token, shows "This link does not work". The page also stops when theoriginin the response is not theoriginin its URL. - The person signs in to Signa ID with an email code.
POST …/signa-release/previewwith{token}. Theoutcomedecides the screen:UNDER_REVIEW: "Your Signa ID is being checked" and "Close". The copy never names the type of check.NOT_COVERED: "Your Signa ID cannot share this" and "Close".READY: "Review what you share". AREADYanswer also hasconsent_hash: the service's SHA-256 (64 lowercase hex characters) of the release terms and the verification. AREADYanswer without a validconsent_hashshows "Something went wrong".
- "Review what you share" shows the business name, the host that gets the
data, the reason, "Access for 30 days, until ", what the business
gets (
data_shared) and the verification card. It shows only the requested scopes. - "Share" sends
POST …/signa-release/consentwith{token, verification_id, consent_hash}. The service calculates the hash again for the current terms. A different hash gets a refusal, which shows on the screen.- The service accepts the consent only from a sign-in in the last 10
minutes, or a confirmation in that time. If not, it answers 403 with
code: "confirmation_required", and the page asks for a new email code (/api/v1/signa-id/confirm/emailand/confirm/email/verify). Then the page sends the consent one more time. If the answer isconfirmation_requiredagain, the page shows an error and does not ask again until the person selects "Share".
- The service accepts the consent only from a sign-in in the last 10
minutes, or a confirmation in that time. If not, it answers 403 with
- The page runs the normal liveness start and complete
(
/api/v1/kyc-capture/startand/complete) on the same token. A cancelled selfie goes back to "Review what you share". POST …/signa-release/completewith{token}returns{release_id, code, code_expires_at}. The page sends{ type: 'signa:release', nonce, release_id, code }to the origin from thesessionresponse, with the nonce that the SDK answered. The SDK removes the dialog or closes the popup.- "Decline" calls
POST …/signa-release/declinewith{token}and shows "You did not share your data" with "Close". As with "under review" and "not covered", the page closes only when the person selects "Close", so the time it stays open says nothing to your page.
Rules:
- The SDK accepts
signa:releaseonly with a nonce that it answered, a uuidrelease_idand a 43-charactercode. - The page sends nothing for "under review", "not covered", a decline, a
refusal or a close, so your page cannot tell them apart. Each one rejects
with
code: 'closed'when the dialog or popup closes. - When the SDK sees the dialog or popup closed, it waits one more check (500 ms) before it rejects, so a release sent just before the close still arrives.
/r/<request_token>refuses to work inside a frame, and/embed/releaseworks only inside one. Anoriginthat is not an exact https origin (orhttp://localhostwith a port) shows "This link does not work".- The request token is a bearer credential for the request. Do not put it in a URL on your site, and do not log it.
Open a KYC session
A business that uses Signa (KYC as a service) can open a session on its own page. The SDK shows the Blaaiz capture page, with the same screens as the session's verification link, in a dialog (the default) or a popup.
On your server, create the session:
POST /api/external/compliance/kyc/sessions.On your server, get an access token for the session:
POST /api/external/compliance/kyc/sessions/<id>/access-token. The answer is{ access_token, expires_at }.Send the
access_tokento your page.On the page, call
signa.startSession(). Inpopupmode, call it from the click handler of a button, or the browser blocks the window:button.onclick = async () => { try { const { sessionId } = await signa.startSession({ accessToken }); // The person finished the steps. Your server gets the result. } catch (error) { // error.code is 'closed', 'blocked' (popup mode only) or 'in_progress'. } };On your server, read the result from the KYC webhook or from
GET /api/external/compliance/kyc/sessions/<id>.
The types:
interface SignaSessionOptions {
accessToken: string; // the access_token, 43 to 64 base64url characters
mode?: 'embedded' | 'popup'; // the default is 'embedded'
}
interface SignaSession {
sessionId: string; // uuid
}How startSession() works:
- The SDK shows the capture page at once, before any request. A popup
opens
/c/<access_token>?origin=<the page's origin>. The dialog's iframe loads/c/embed?origin=<the page's origin>, with no token in the URL. - The capture page sends
{ type: 'signa:hello', nonce }toorigin. The SDK answers{ type: 'signa:ready', nonce }. Inembeddedmode, the answer also carriesaccess_token, so the token never appears in your page's DOM. - The person does the steps of the session.
- When the steps are done, the capture page sends
{ type: 'signa:session', nonce, session_id }toorigin, with the nonce that the SDK answered. The SDK resolves withsessionId. - The capture page shows its done screen. After 5 seconds, or when the
person selects "Continue", the popup closes. In
embeddedmode, the page sends{ type: 'signa:close', nonce }and the SDK removes the dialog.
Rules:
- The access token is the capture token of the session's verification link. The token is valid for 30 minutes from when it was made.
- The access token call returns the same token while it has more than
10 minutes left. After that, it makes a new token and stops the old one.
A popup that is still open on the old token stops working. When the person
closes it, the promise rejects with
code: 'closed'. - A verification link call makes a new capture token and stops the access token.
- Only a session on the Blaaiz capture page has an access token. For other
sessions, the access token call answers
422. - A resolved promise does not give a result. It tells you only that the person finished the steps. Your server must read the result.
- On your server, use the session id that you stored when you created the
session for this person. Do not trust a
sessionIdthat your page sends to your server: a person can change it to the id of a different session. - The access token is a bearer credential for the person's check. Do not put it in a URL on your site, and do not log it.
- The capture page sends no session data to your page before the steps are
done. If the person closes the popup or the dialog (with its close button
or Escape), the promise rejects with
code: 'closed'. The session stays open, and the same access token works until it expires. - "Use Signa ID" on the capture page works in both modes. The person signs in with an email code each time.
- When the SDK opened the popup, the popup does not go to the session's
redirect_url. When the link opens in a new tab, the page works as a verification link.
Development
npm ci
npm run typecheck
npm test
npm run build # dist/: index.js, index.cjs, signa-web-sdk.min.js and the .d.ts files
npm run test:dist # loads each build the way an integrator doesThe SDK and the Signa ID pages share the message protocol in
src/protocol.ts. The frontend keeps a copy for its pages. A protocol change
must land in both repositories, pages first, and the pages must keep
accepting the messages of every published SDK version.
Publishing
The Publish workflow publishes the package to npm when a GitHub release
is published from main. It needs the NPM_TOKEN repository secret: an npm
automation token with publish rights on the @blaaiz scope. Before the
release, set version in package.json and give the version's section in
CHANGELOG.md its date. Name the release tag after the version
(v1.0.0 for 1.0.0): the workflow stops when they differ. jsDelivr and
unpkg serve the standalone build from npm, so a release is on the CDN too.
Branches
dev is the integration branch and main is production. Open pull requests
against dev.
