Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
README.md
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | # ckey: CKey for the web |
| 2 | |
| 3 | Encode, render and scan [CKey](https://openckey.org) optical codes in the browser, in workers and in Node. The package is the CKey C core compiled to WebAssembly, plus a small JavaScript wrapper. Every encode, decode and render call runs in the core. The wrapper only moves bytes in and out. |
| 4 | |
| 5 | - Headless rendering to native pixels: `ImageData` on a `<canvas>` or `OffscreenCanvas`, or raw RGBA. There is no SVG step and no DOM work. |
| 6 | - Exports: PNG bytes, an SVG string and PDF vector bytes. |
| 7 | - Scanning of RGBA camera frames. It finds the border, refines it for each profile and decodes, trying Profile 0 first and then Profiles 1, 2 and 3. |
| 8 | - Optional aggregate telemetry. You can turn it off with one line (see [Privacy and telemetry](#privacy-and-telemetry)). |
| 9 | |
| 10 | ## Install |
| 11 | |
| 12 | ```sh |
| 13 | npm install openckey |
| 14 | ``` |
| 15 | |
| 16 | The package is ESM only. `dist/ckey.mjs` loads `dist/ckey.wasm` from its own directory, and bundlers that understand `new URL('./ckey.wasm', import.meta.url)` copy the binary for you (Vite, webpack 5, Rollup with a URL plugin, esbuild with a file loader). To serve the binary from somewhere else, pass `wasmUrl`. You can also pass the bytes as `wasmBinary`. |
| 17 | |
| 18 | ```js |
| 19 | const ckey = await loadCKey({ wasmUrl: '/static/ckey.wasm' }); |
| 20 | ``` |
| 21 | |
| 22 | ## Usage |
| 23 | |
| 24 | ### Make a code |
| 25 | |
| 26 | ```js |
| 27 | import { loadCKey } from 'openckey'; |
| 28 | |
| 29 | const ckey = await loadCKey(); |
| 30 | |
| 31 | // text (UTF-8) or bytes; version 0 picks the smallest that fits |
| 32 | const sym = ckey.encode('https://openckey.org', { version: 0, level: 'M', layout: 'standard' }); |
| 33 | console.log(sym.version, sym.level, sym.layout, sym.payloadLength, sym.capacity); |
| 34 | |
| 35 | // draw into a canvas: ImageData + putImageData, no intermediate file |
| 36 | ckey.renderToCanvas(sym, document.querySelector('canvas'), { size: 512, transparent: false, policy: 'coverage' }); |
| 37 | |
| 38 | // or use the pixels yourself (also works in Node and in workers) |
| 39 | const { width, height, data } = ckey.renderPixels(sym, { size: 1024 }); |
| 40 | |
| 41 | // exports |
| 42 | const png = ckey.renderPNG(sym, { size: 1024, transparent: true }); // Uint8Array |
| 43 | const svg = ckey.renderSVG(sym, { preset: 'concept' }); // string |
| 44 | const pdf = ckey.renderPDF(sym, { widthPt: 144 }); // Uint8Array, 2 inches wide |
| 45 | |
| 46 | sym.free(); // release WebAssembly memory now; a forgotten symbol is freed by the GC |
| 47 | ``` |
| 48 | |
| 49 | `encode` throws an `Error` that carries the core's diagnostic message, for example `payload exceeds the selected Profile 0 version and level`. An invalid option throws a `TypeError` or a `RangeError`. |
| 50 | |
| 51 | #### Render options |
| 52 | |
| 53 | | Option | Values | Default | |
| 54 | |---|---|---| |
| 55 | | `size` | 16 to 16384 px across the full 31-unit canvas, quiet margin included | 512 | |
| 56 | | `policy` | `'coverage'` (antialiased), `'binary'` or `'hybrid'` (thin marks widened to `minFeaturePx`) | `'coverage'` | |
| 57 | | `transparent` | `true` paints no background, so unpainted pixels have alpha 0 | `false` | |
| 58 | | `preset` | `'plain'` (black on white) or `'concept'` | `'plain'` | |
| 59 | | `style` | `{ background, backgroundEdge, ink, inkShade, accents, accentPercent, modules, rings, rays, bevel, shadow, texture, logoHref, logoRadius, logoPlate, policy }`, applied on top of the preset | none | |
| 60 | | `supersample`, `minFeaturePx`, `materials`, `logo` | advanced raster settings (see `types/ckey.d.ts`) | 4, 1, `true`, none | |
| 61 | |
| 62 | Colours are written as `'#rrggbb'`, `[r, g, b]` or `{ r, g, b }`. Styling changes how a symbol is painted, never where a reader samples. The core checks every style. `style.policy` sets how a style that fails the check is handled: |
| 63 | |
| 64 | - `'strict'` (the default) refuses it, and the error message explains why. |
| 65 | - `'adaptive'` repairs it. |
| 66 | - `'override'` draws it as given and leaves the symbol uncertified. |
| 67 | |
| 68 | `ckey.checkStyle(sym, { style })` returns the core's verdict. `ckey.certify(sym, { transparent, backdrop })` runs the core's modelled-scan certification. A transparent symbol is never certified unless you describe what it sits on (`backdrop.colours` or `backdrop.pixels`). |
| 69 | |
| 70 | ### Scan |
| 71 | |
| 72 | ```js |
| 73 | const scanner = ckey.createScanner({ |
| 74 | onResult(result) { |
| 75 | // result: { bytes, text, profile: '0' | '0.1' | '1' | '2' | '3', version, layout } |
| 76 | const action = ckey.classifyPayload(result.text ?? result.bytes); |
| 77 | showResult(result, action); // never act automatically: let the user tap |
| 78 | }, |
| 79 | }); |
| 80 | |
| 81 | // for each camera frame (e.g. from requestVideoFrameCallback + a canvas) |
| 82 | ctx.drawImage(video, 0, 0); |
| 83 | scanner.processFrame(ctx.getImageData(0, 0, canvas.width, canvas.height)); |
| 84 | |
| 85 | scanner.reset(); // scan again after a result |
| 86 | scanner.stop(); // camera closed |
| 87 | ``` |
| 88 | |
| 89 | For a single image, call `ckey.decodeImageData(imageData)`. It returns `null` when no CKey is found, and it also accepts `{ width, height, data }`. RGBA pixels are composited over white and converted to Rec. 709 luma before decoding. If you already have 8-bit grayscale, `ckey.decodeGray({ width, height, data })` skips that conversion. |
| 90 | |
| 91 | `text` holds the payload as text when it is valid UTF-8 with no control characters other than tab, LF and CR. Otherwise it is `null`, and you can show `bytes` as hex instead (`toHex` is exported). |
| 92 | |
| 93 | ### Interpret a payload |
| 94 | |
| 95 | `classifyPayload(text)` follows the [CKey Payload Conventions](../../spec/CKey-Payload-Conventions.md). It returns `{ kind, target, label }`, where `kind` is one of `https`, `http`, `mailto`, `tel`, `sms`, `geo`, `address`, `vcard` or `text`. It only describes an action, such as `"Open openckey.org"` or `"Call +15551234567"`, and never performs it. Schemes that are not on the allowlist (`javascript:`, `data:`, `file:`, `intent:`, custom schemes) come back as plain text, as do forms that fail validation. The function is also exported from the module, so you can call it without loading the core. |
| 96 | |
| 97 | ## Safe Actions |
| 98 | |
| 99 | A Safe Action (`ckey-action:1:…`) is a signed code from a known site for this app. `ckey.safeActions()` verifies it with the core and decides whether it runs; the page only pins keys and registers handlers. |
| 100 | |
| 101 | ```js |
| 102 | const actions = ckey.safeActions('com.viadere') |
| 103 | .trust('citeory.com', '2026-11', '<64 hex digits>', { requestInfo: 'https://citeory.com/api/ckey/requests/{request_id}' }) |
| 104 | .register('open.library', { issuer: 'citeory.com' }, (action) => openItem(action.paramsJSON)) |
| 105 | .register('signin.request', { issuer: 'citeory.com', scope: CROSS_DEVICE }, (action) => approveSignIn(action)); |
| 106 | |
| 107 | const outcome = await actions.handle(text); |
| 108 | if (outcome.kind === 'not-safe-action') showPayload(text); // the ordinary payload rules |
| 109 | else if (outcome.kind === 'confirm') await outcome.request.present(); // the SDK's own <dialog> |
| 110 | else if (outcome.kind === 'rejected') showUnverified(outcome.reason); |
| 111 | ``` |
| 112 | |
| 113 | Same-device actions run at once. Cross-device actions run only from `request.present()`, which shows the verified issuer and who is asking; there is no other way to approve one. Anything that fails a check runs nothing; show that the code claims to be a Safe Action but could not be verified, with the reason. See [spec/CKey-Safe-Actions.md](../../spec/CKey-Safe-Actions.md). |
| 114 | |
| 115 | ## Privacy and telemetry |
| 116 | |
| 117 | **Opt out with one line:** |
| 118 | |
| 119 | ```js |
| 120 | ckey.telemetry = 0; |
| 121 | ``` |
| 122 | |
| 123 | You can also opt out at load time with `loadCKey({ telemetry: 0 })`, which applies before anything is counted. |
| 124 | |
| 125 | | `ckey.telemetry` | Counts locally | Sends | |
| 126 | |---|---|---| |
| 127 | | `0` | no | no | |
| 128 | | `1` (default) | yes | aggregate batches, at most one per hour | |
| 129 | | `2` | yes | never (`ckey.telemetryCounters()` returns the counts) | |
| 130 | |
| 131 | Any other value throws a `RangeError`. The mode is never silently reset to 1. |
| 132 | |
| 133 | Only the scanner helper (`createScanner`) records anything. Encoding, rendering and one-off `decodeImageData` calls never do. A scan session starts at the first frame, succeeds at the first decode and ends at `stop()`. A session with no decode for 30 seconds fails. |
| 134 | |
| 135 | A batch holds only these fields: |
| 136 | |
| 137 | - session counts; |
| 138 | - the number of frames; |
| 139 | - decodes per profile; |
| 140 | - two duration histograms; |
| 141 | - `sdk: "web"`, the SDK version and `platform: "web"`; |
| 142 | - the UTC day. |
| 143 | |
| 144 | A batch never contains payloads, URLs, identifiers, fingerprints or location. Batches go by `fetch` POST to `https://openckey.org/api/telemetry/v1/batch` with `keepalive`, no cookies and no `Referer`. A send runs asynchronously, never blocks scanning, and is also tried when the page is hidden. |
| 145 | |
| 146 | The full rules are in [docs/adoption-and-telemetry.md](../../docs/adoption-and-telemetry.md). Your own privacy disclosures may still need to mention telemetry while mode 1 is on. |
| 147 | |
| 148 | For tests or a custom transport, pass `loadCKey({ fetch })`. `fetch: null` never sends. |
| 149 | |
| 150 | ## Build from source |
| 151 | |
| 152 | You need [Emscripten](https://emscripten.org) (`emcmake` and `em++` on `PATH`), CMake 3.20 or later and Node 22. |
| 153 | |
| 154 | ```sh |
| 155 | npm run build # emcmake cmake + build, then assemble dist/ |
| 156 | npm test # node:test; the wasm tests skip when dist/ is missing |
| 157 | npm run test:native # bridge + core compiled with the system C++ compiler |
| 158 | npm run check # node --check every JS file |
| 159 | ``` |
| 160 | |
| 161 | | Path | Contents | |
| 162 | |---|---| |
| 163 | | `src/ckey_web.cpp` | the bridge: plain exported C functions over the public C ABI | |
| 164 | | `js/` | the wrapper (`ckey.mjs` loader, `api.mjs`, and pure modules for options, payloads, telemetry and the scanner) | |
| 165 | | `types/ckey.d.ts` | TypeScript declarations, copied to `dist/` | |
| 166 | |
| 167 | ## License |
| 168 | |
| 169 | MIT. See [LICENSE](LICENSE). |