For developers
Let people sign on your website with the certificate they already have. You build the document and its hash; WebeSign returns the signature and the certificate.
Quickstart: your website
Install the package. It has no dependencies, is fully typed and adds under 5 KB to your page.
bun add @websign/sdk # or: npm install @websign/sdk
Then sign. prepare receives the certificate the person chose and returns the hash to sign:
import { sign } from "@websign/sdk";
const { signature, certificate } = await sign({
hash: "SHA-256",
prepare: (certificate, { hash }) => crypto.subtle.digest(hash, signedAttributesFor(certificate)),
});
result.signature is the raw signature (ECDSA r‖s, or the RSA block). Put it in your CMS or PDF together with result.certificate.der and result.certificate.chain.
How prepare works
- Your page calls sign(). The WebeSign window opens and the person chooses a certificate.
- prepare(certificate) runs in your page. PAdES and CAdES put the certificate inside the signed data, so this is where you build the signed attributes and return their hash (32, 48 or 64 bytes).
- The person compares the verification code and confirms, typing the PIN in the app if needed. The document never leaves your page.
- sign() resolves with the signature. If the person switches certificate, prepare runs again and only the last hash is signed.
The types follow your options: with hash: "SHA-384" and algorithm: "ECDSA", prepare's context and the result are typed with exactly those values.
Show the Sign button only when WebeSign is ready
status() never opens a window and never fails. problem says what is missing; onChange tells you when the person installs or updates something while your page is open.
import { installUrl, onChange, status } from "@websign/sdk";
import { errorText } from "@websign/sdk/messages";
function show({ ready, problem }) {
signButton.hidden = !ready;
installLink.hidden = problem !== "ExtensionMissing";
installLink.href = installUrl(); // the extension store of this browser
notice.textContent = errorText(problem)?.body ?? "";
}
show(await status());
onChange(show);
Errors
Every failure is a WebSignError with a stable code, a message and a hint for you, and a docsUrl that points to this table. For the person, errorText(error) from @websign/sdk/messages gives a title and the next step in 7 languages.
import { isWebSignError, sign } from "@websign/sdk";
import { errorText } from "@websign/sdk/messages";
try {
await sign({ hash: "SHA-256", prepare });
} catch (error) {
if (!isWebSignError(error)) throw error;
if (error.code === "UserCancelled") return; // nothing to show
console.warn(error.code, error.hint, error.docsUrl); // for you
const text = errorText(error); // for the person, localized
if (text) showBanner(text.title, text.body);
}
| Code | What happened and what to do |
|---|---|
ExtensionMissing | The WebeSign extension did not answer: it is not installed, or it is disabled for this site. Show a link to installUrl(). |
AppMissing | The extension is there but the WebeSign app is not. The person installs the app; the extension guides them. |
AppOutdated | The app is older than the extension needs (details has both versions). The person updates the app. |
ExtensionOutdated | The extension is older than the app needs. The person restarts the browser so it updates. |
ClientOutdated | The extension only speaks a newer protocol than your copy of @websign/sdk. Update the package in your site. |
InsecureOrigin | The page is not served over https. Serve it over https (http://localhost also works). |
Aborted | Your AbortSignal fired, or prepare threw (your error is in cause). Nothing to show the person. |
UserCancelled | The person closed the window. Leave the page as it was; no message is needed. |
Timeout | Nobody confirmed in time, or prepare took more than 60 seconds. Offer to try again. |
NoCertificates | No usable certificate was found. The person plugs in the token or card and tries again. |
CertificateUnavailable | The chosen certificate is gone (the token was removed). The person plugs it back in or chooses another one. |
CertificateNotValid | The certificate has expired or is not valid yet. The person chooses another one. |
InvalidRequest | A bug in the call: a wrong option, or a hash of the wrong length. The message says which. |
UnsupportedAlgorithm | The key cannot produce the algorithm you asked for. Accept more algorithms, or ask for another certificate. |
PinIncorrect | A wrong PIN. The app asks again, so a website rarely sees this. |
PinLocked | The PIN is blocked. The person unlocks it with the PUK, using the issuer's tools. |
TokenRemoved | The token was removed while signing. The person plugs it back in and tries again. |
DriverFailure | The token driver failed; details.native has its status. Try again; if it keeps failing, the person reconnects the token. |
Busy | Other requests are waiting in the WebeSign window. Try again once they are done. |
Internal | A bug in WebeSign. Please report it with the diagnostics from the app. |
Test without the extension
@websign/sdk/testing installs a fake extension and app on window. Your code runs unchanged: status(), certificates() and sign() work, prepare runs, and the signatures are real ECDSA or RSA made with public test keys, so you can verify them. Script every outcome: the person cancels, no certificates, app missing, outdated, driver errors.
import { installFakeWebSign, type FakeWebSign } from "@websign/sdk/testing";
let fake: FakeWebSign;
beforeEach(async () => (fake = await installFakeWebSign())); // in jsdom or happy-dom
afterEach(() => fake.uninstall());
it("signs the contract", async () => {
const result = await signContract(); // your code, calling sign()
expect(await fake.verify(result, signedAttributes)).toBe(true);
});
it("stays calm when the person cancels", async () => {
fake.failNext("UserCancelled");
await expect(signContract()).rejects.toMatchObject({ code: "UserCancelled" });
});
// Other setups: installFakeWebSign({ scenario: "app-missing" | "app-outdated" | "extension-missing" | … }),
// installFakeWebSign({ certificates: [] }), fake.switchDuringNextSign(1), fake.requests.
The same fake lets you build your page before installing anything. Load it only in development:
if (import.meta.env.DEV) {
const { installFakeWebSign } = await import("@websign/sdk/testing");
await installFakeWebSign();
}
Never ship the fake. Anyone can sign with its keys. It refuses to run outside localhost-like addresses and warns on the console when installed.
React and Vue
The SDK has no framework code. The examples include a copy-paste useWebSign() hook for React and the same as a Vue composable: live status, progress, error, and cancelling when the component goes away.
const { status, signing, error, sign } = useWebSign();
<button disabled={!status?.ready || signing} onClick={() => sign({ hash: "SHA-256", prepare })}>
Sign
</button>
Signing PDFs (PAdES)
Your PDF library reserves room for the signature and hashes the rest of the file. In prepare you build the signed attributes for the chosen certificate and return their hash; the signature you get back goes into the CMS inside the PDF. The PAdES example does this with pdf-lib and PKI.js, and its test checks the result with OpenSSL.
API reference
Every function and type of @websign/sdk, @websign/sdk/messages and @websign/sdk/testing, with examples, generated from the source.
Desktop programs
Desktop program (Node, Bun, Electron): @websign/desktop
Connect to the app, then call sign(). The app names your program in its confirmation window.
import { createHash } from "node:crypto";
import { WebSign } from "@websign/desktop";
const websign = await WebSign.connect(); // AppMissing if not installed
try {
const { signature, certificate } = await websign.sign({
hash: "SHA-256",
prepare: () => createHash("sha256").update("my document").digest(),
});
console.log(`${certificate.displayName}: ${signature.byteLength} bytes`);
} finally {
await websign.close();
}
Desktop program (Rust): websign-client
Client::connect(), then certificates() or sign(). The app is started on demand.
API guides and contracts in the repository: docs/architecture/web-api.md and docs/architecture/desktop-api.md