@writermark/sdk
v0.6.4
Published
Writermark client SDK — human authorship verification for text editors
Readme
@writermark/sdk
Human-authorship certification for text editors. The SDK observes how users type and issues cryptographic certificates proving a human wrote the text. No text content is stored on the server.
Status: Alpha — this SDK is in early development. Breaking changes may occur before the official 1.0.0 release. Reach out at [email protected] if you need help getting started.
Install
npm install @writermark/sdkOptional for DOCX verification support:
npm install jszipQuick Start: React + TipTap
The fastest path. One hook, one component, ~15 lines of code.
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { useWritermark, CertIndicator } from '@writermark/sdk/react'
function MyEditor() {
const editor = useEditor({ extensions: [StarterKit] })
const { status, certificate, certifyNow } = useWritermark('doc-1', editor, {
onCheckpoint: (checkpoint, coverage, pass, cert, map, checkpoints) => {
localStorage.setItem('my-checkpoint', checkpoint)
localStorage.setItem('my-checkpoints', JSON.stringify(checkpoints))
},
previousCheckpoint: localStorage.getItem('my-checkpoint'),
previousCheckpoints: JSON.parse(localStorage.getItem('my-checkpoints') || 'null'),
})
return (
<div>
<EditorContent editor={editor} />
<CertIndicator status={status} certificate={certificate} certifyNow={certifyNow} />
</div>
)
}That's it. The hook handles the full certification lifecycle — collecting behavioral telemetry, sending it to the server every 30 seconds, and maintaining a signed checkpoint chain. The CertIndicator shows a colored dot with the current status and a hover panel with score details.
useWritermark options
| Prop | Type | Description |
|------|------|-------------|
| writermarkUrl | string? | API URL (defaults to https://api.writermark.org) |
| onCheckpoint | function | Called after each certification with (checkpoint, coverage, pass, certificate, authorshipMap, checkpoints, vdfState) |
| previousCheckpoint | string | null | Restore a saved checkpoint to continue certification across sessions |
| previousCheckpoints | string[] | null | Rolling window of recent checkpoint JWTs (max 2). Falls back to previousCheckpoint if not provided. |
| previousPass | boolean | Whether the previous checkpoint was passing |
| previousAuthorshipMap | AuthorshipMap | null | Saved authorship map from a previous session |
| previousVdfState | VdfState | null | VDF temporal proof state (not yet integrated) |
| debug | boolean | Log certification details to console |
useWritermark return values
| Field | Type | Description | |-------|------|-------------| | status | 'idle' | 'certifying' | 'certified' | 'not-certified' | Current certification state | | coverage | number | null | Fraction of text covered by observed keystrokes (0–1) | | certificate | string | null | Signed attestation JWT when passing | | checkpoint | string | null | Latest signed checkpoint JWT | | certifyNow | () => Promise | Force an immediate certification cycle | | authorshipMap | AuthorshipMap | null | RLE array tracking provenance of each character | | isTracking | boolean | Whether the collector is attached and running |
TipTap without React
Use WritermarkSession — one constructor, full lifecycle handled for you. No manual fetch loops.
import { WritermarkSession } from '@writermark/sdk'
const session = new WritermarkSession(editor, {
documentId: 'my-doc',
onCheckpoint: (checkpoint, coverage, pass, cert, map, checkpoints) => {
myDatabase.save({ checkpoint, checkpoints, authorshipMap: map })
},
onStatusChange: (status) => {
console.log('certification status:', status)
},
previousCheckpoint: myDatabase.get('checkpoint'),
previousCheckpoints: myDatabase.get('checkpoints'),
})
// Force immediate certification (e.g. before publish or export)
const result = await session.certifyNow()
// Clean up when done
session.destroy()WritermarkSession handles event collection, compression, the 30-second certification loop, checkpoint chain management, clipboard enrichment on copy, and certified paste detection — all internally. Same class works for all editor types.
WritermarkSession options
| Option | Type | Description |
|--------|------|-------------|
| documentId | string | Persistent identifier for the document (required) |
| writermarkUrl | string? | API URL (defaults to https://api.writermark.org) |
| onCheckpoint | function | Called after each certification with (checkpoint, coverage, pass, certificate, authorshipMap, checkpoints, vdfState) |
| onStatusChange | function | Called when status changes: 'idle' | 'certifying' | 'certified' | 'not-certified' |
| onCertifyResult | function | Called with the full server response object after each certification |
| previousCheckpoint | string | null | Restore a saved checkpoint to continue across sessions |
| previousCheckpoints | string[] | null | Rolling window of recent checkpoint JWTs (max 2) |
| previousPass | boolean | Whether the previous checkpoint was passing |
| previousAuthorshipMap | AuthorshipMap | null | Saved authorship map from a previous session |
| previousVdfState | VdfState | null | VDF temporal proof state (not yet integrated) |
| getText | () => string | Custom text getter (auto-detected for TipTap, textarea, contenteditable) |
| debug | boolean | Log certification details to console |
WritermarkSession methods
| Method | Returns | Description | |--------|---------|-------------| | certifyNow() | Promise<object | null> | Force an immediate certification cycle. Call before exporting or copying a certificate to ensure it matches the latest text. | | getStatus() | CertificationStatus | Current certification state | | getCertificate() | string | null | Latest attestation JWT | | getCheckpoint() | string | null | Latest checkpoint JWT | | getCheckpoints() | string[] | Full rolling checkpoint window | | getVdfState() | VdfState | null | Current VDF state (not yet integrated) | | getAuthorshipMap() | AuthorshipMap | null | Current authorship map | | getCoverage() | number | null | Keystroke coverage fraction (0–1) | | isActive() | boolean | Whether the session is running | | destroy() | void | Stop the session and clean up all listeners |
Generic DOM (textarea / contenteditable)
Pass any <textarea>, <input>, or contenteditable element. The session auto-detects the element type.
import { WritermarkSession } from '@writermark/sdk'
const session = new WritermarkSession(
document.querySelector('#my-editor'),
{
documentId: 'my-doc',
onCheckpoint: (checkpoint, coverage, pass) => {
localStorage.setItem('checkpoint', checkpoint)
},
}
)
session.destroy()Note: clipboard enrichment on copy (certified paste provenance) requires TipTap.
Browser Script Tag
For non-bundled environments, load the SDK as a script tag. Everything is available on window.Writermark.
<script src="https://writermark.org/sdk.js"></script>
<script>
var session = new Writermark.WritermarkSession(
document.querySelector('#my-editor'),
{
documentId: 'my-doc',
onCheckpoint: function(checkpoint) {
localStorage.setItem('wm-checkpoint', checkpoint)
},
}
)
</script>Verification
The SDK includes a complete verification toolkit.
Verify a file (golden path)
Hand it a File object from an <input type="file"> or drag-and-drop. Supports .wtxt, .docx, .rtf, .md, .txt, and .html.
import { verifyFile } from '@writermark/sdk'
const input = document.querySelector('input[type="file"]')
input.addEventListener('change', async () => {
const result = await verifyFile(input.files[0])
if (result.valid) {
console.log('Verified!', result.score, result.confidence)
} else {
console.log('Failed:', result.detail)
}
})DOCX support requires JSZip as an optional dependency.
Verify text + token
import { verify } from '@writermark/sdk'
const result = await verify({
token: 'eyJhbGciOi...', // raw JWT or full certificate text
text: 'The original document text...',
})
console.log(result.valid) // true/false
console.log(result.signatureOnly) // false (text was checked too)
console.log(result.score) // 0.82
console.log(result.confidence) // 0.88Verify token only (signature check)
const result = await verify({ token: certificateText })
// result.signatureOnly === trueExtract without verifying
import { extractFromFile } from '@writermark/sdk'
const { text, token, fileType } = await extractFromFile(file)
// fileType: 'wtxt' | 'docx' | 'rtf' | 'md' | 'txt' | 'html'VerifyResult
| Field | Type | Description | |-------|------|-------------| | valid | boolean | Whether the certificate is valid | | pass | boolean? | Whether the document passed certification | | score | number? | Human-authorship score (0–1) | | confidence | number? | Confidence level (0–1) | | issuedAt | string? | ISO timestamp of certification | | signatureOnly | boolean | True if only the signature was verified (no text hash check) | | detail | string? | Human-readable detail on failure |
verifyFile() also returns text, token, and fileType on the result object.
Custom Editor (Manual API)
For editors that aren't TipTap or standard DOM elements, use the Collector directly. Call record* methods from your editor's event handlers.
import { Collector } from '@writermark/sdk'
const collector = new Collector()
collector.start()
collector.recordKey('KeyA', cursorPosition)
collector.recordKeyUp('KeyA')
collector.recordBackspace('Backspace', cursorPosition)
collector.recordEnter('Enter', cursorPosition)
collector.recordPaste(charCount, pastedText, 'external', cursorPosition)
collector.recordCopyOrCut(selectedText, copyStart, copyLength, isCut)
collector.recordCursorJump(distance)
collector.recordSelect(selectionLength)
collector.recordUndo()
collector.recordRedo()
collector.recordFocus()
collector.recordBlur()
collector.recordScroll(deltaY)
collector.recordMouse(xRatio, yRatio) // 0-1 normalized
collector.recordVisibility(isVisible)
collector.recordMutation(position, deleteLength, insertLength, 'typed')The recordMutation method is key — it tracks document changes for the authorship map. The insertSource parameter should be 'typed', 'paste-internal', 'paste-external', 'paste-certified', 'undo', or 'redo'.
API Reference
All API endpoints are served from https://api.writermark.org.
POST /certify
The main certification endpoint. Stateless — all state lives in the signed checkpoint. Rate limit: 30/min per IP.
Request body:
| Field | Type | Description | |-------|------|-------------| | documentId | string | Persistent document identifier (required on first call) | | events | EditorEvent[] | New events since last checkpoint | | checkpoints | string[] | Rolling window of recent checkpoint JWTs (max 2). Empty array on first call. | | recentEvents | EditorEvent[] | Events from the previous window (for ML context). Empty on first call. | | merkleRoot | string | null | Merkle root of document chunks | | authorshipMap | AuthorshipMap | null | RLE authorship intervals | | textHash | string | SHA-256 of normalized text | | charCount | number | Current text length | | vdfState | VdfState | null | VDF temporal proof state (not yet integrated) |
The daemon also accepts legacy fields checkpoint (singular) and contentMerkleRoot for backward compatibility.
Response:
{
"checkpoint": "eyJhbGciOi...",
"score": 0.72,
"behavioralScore": 0.85,
"coverage": 0.95,
"pass": true,
"certificate": "eyJhbGciOi...",
"authorshipMap": [[0, 150, "human_evidenced"]],
"confidence": 0.88,
"activeWritingTimeMs": 120000,
"revisionPercent": 8.5
}POST /verify
Verify a certificate's signature. Body: { "token": "eyJ..." }
POST /verify-text
Verify a certificate matches specific text. Body: { "text": "...", "token": "eyJ..." }
POST /derive
Create a standalone certificate for an excerpt (e.g. copy-paste certification). Requires a valid source JWT and Merkle proofs for the excerpt's chunks.
GET /public-key
Returns the Ed25519 public key for verifying attestation JWTs.
{
"publicKey": {
"kty": "OKP",
"crv": "Ed25519",
"x": "..."
}
}GET /.well-known/jwks.json
Standard JWKS endpoint for public key discovery. Key ID: writermark-v1.
Components
CertIndicator
Drop-in React component showing a colored status dot, label, and hover popup with certificate details.
import { CertIndicator } from '@writermark/sdk/react'
<CertIndicator
status={status}
certificate={certificate}
certifyNow={certifyNow}
/>| Prop | Type | Description |
|------|------|-------------|
| status | CertificationStatus | From useWritermark |
| certificate | string | null | Attestation JWT from useWritermark |
| certifyNow | () => Promise? | If provided, the indicator runs an immediate certification cycle before copying or viewing. Pass certifyNow from useWritermark. |
| writermarkUrl | string? | Server URL for "View certificate" link (defaults to writermark.org) |
| className | string? | CSS class override for the wrapper element |
Links
- Writermark — Verify certificates and try the demo
- Documentation — Full developer docs
- Wintertext — Free desktop writing app with Writermark built in
License
MIT
