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

Size
9.1 KB
Lines
169
Language
markdown
SHA-256
5068f8d993a6…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1# ckey: CKey for the web
2
3Encode, 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
13npm install openckey
14```
15
16The 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
19const ckey = await loadCKey({ wasmUrl: '/static/ckey.wasm' });
20```
21
22## Usage
23
24### Make a code
25
26```js
27import { loadCKey } from 'openckey';
28
29const ckey = await loadCKey();
30
31// text (UTF-8) or bytes; version 0 picks the smallest that fits
32const sym = ckey.encode('https://openckey.org', { version: 0, level: 'M', layout: 'standard' });
33console.log(sym.version, sym.level, sym.layout, sym.payloadLength, sym.capacity);
34
35// draw into a canvas: ImageData + putImageData, no intermediate file
36ckey.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)
39const { width, height, data } = ckey.renderPixels(sym, { size: 1024 });
40
41// exports
42const png = ckey.renderPNG(sym, { size: 1024, transparent: true }); // Uint8Array
43const svg = ckey.renderSVG(sym, { preset: 'concept' }); // string
44const pdf = ckey.renderPDF(sym, { widthPt: 144 }); // Uint8Array, 2 inches wide
45
46sym.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
62Colours 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
73const 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)
82ctx.drawImage(video, 0, 0);
83scanner.processFrame(ctx.getImageData(0, 0, canvas.width, canvas.height));
84
85scanner.reset(); // scan again after a result
86scanner.stop(); // camera closed
87```
88
89For 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
99A 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
102const 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
107const outcome = await actions.handle(text);
108if (outcome.kind === 'not-safe-action') showPayload(text); // the ordinary payload rules
109else if (outcome.kind === 'confirm') await outcome.request.present(); // the SDK's own <dialog>
110else if (outcome.kind === 'rejected') showUnverified(outcome.reason);
111```
112
113Same-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
120ckey.telemetry = 0;
121```
122
123You 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
131Any other value throws a `RangeError`. The mode is never silently reset to 1.
132
133Only 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
135A 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
144A 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
146The 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
148For tests or a custom transport, pass `loadCKey({ fetch })`. `fetch: null` never sends.
149
150## Build from source
151
152You need [Emscripten](https://emscripten.org) (`emcmake` and `em++` on `PATH`), CMake 3.20 or later and Node 22.
153
154```sh
155npm run build # emcmake cmake + build, then assemble dist/
156npm test # node:test; the wasm tests skip when dist/ is missing
157npm run test:native # bridge + core compiled with the system C++ compiler
158npm 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
169MIT. See [LICENSE](LICENSE).