Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

api.mjs

v0.1.0-build.85 Latest Commit ede38f5

Size
16 KB
Lines
464
Language
javascript
SHA-256
ce8228c6cb13…
View on GitHub

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

1// The CKey Web SDK object, built on an instantiated Emscripten module of the
2// C core plus the bridge in sdk/web/src/ckey_web.cpp. Every encode, decode
3// and render call goes to the core; this file only marshals data.
4
5import {
6 LEVELS,
7 LAYOUTS,
8 certifyOptions,
9 encodeOptions,
10 grayFrame,
11 payloadBytes,
12 pdfOptions,
13 rasterOptions,
14 rgbaFrame,
15 svgOptions,
16 telemetryMode,
17} from './options.mjs';
18import { classifyPayload, readableText } from './payload.mjs';
19import { TelemetrySender, installBackgroundSend, monotonicNow, utcDay } from './telemetry.mjs';
20import { makeScanner } from './scanner.mjs';
21import { SafeActions } from './safe-actions.mjs';
22
23const PROFILES = ['0', '0.1', '1', '2', '3']; // indexed by the core's profile code
24const ADJUSTMENTS = ['ink', 'accents', 'effects', 'logo', 'background', 'materials'];
25const STYLE_POLICY_NAMES = ['strict', 'adaptive', 'override'];
26const utf8 = new TextEncoder();
27const utf8d = new TextDecoder();
28
29/**
30 * @param M instantiated Emscripten module (exports _ckw_* and HEAPU8)
31 * @param options { fetch, telemetryEndpoint, telemetry, now, day, document, window }
32 */
33export function createCKey(M, options = {}) {
34 if (!M || typeof M._ckw_encode !== 'function') throw new Error('CKey core module is missing its exports');
35 if (!M.HEAPU8) throw new Error('CKey core module does not export HEAPU8');
36 const now = options.now || monotonicNow;
37 const heap = () => M.HEAPU8; // re-read after every call: memory may grow
38
39 // ---- memory helpers ------------------------------------------------------
40 const alloc = (n) => {
41 const p = M._ckw_malloc(n);
42 if (!p) throw new Error('CKey: out of memory');
43 return p;
44 };
45 const copyIn = (bytes) => {
46 const p = alloc(bytes.length);
47 heap().set(bytes, p);
48 return p;
49 };
50 const cstring = (p) => {
51 const h = heap();
52 let e = p;
53 while (h[e] !== 0) e++;
54 return utf8d.decode(h.subarray(p, e));
55 };
56 const withCString = (s, fn) => {
57 const b = utf8.encode(s);
58 const p = alloc(b.length + 1);
59 heap().set(b, p);
60 heap()[p + b.length] = 0;
61 try {
62 return fn(p);
63 } finally {
64 M._ckw_free(p);
65 }
66 };
67 const lastError = (fallback) => {
68 const m = cstring(M._ckw_last_error());
69 return m || fallback;
70 };
71 /** Copies a malloc'd result of ckw_last_len() bytes out and frees it. */
72 const takeOut = (p, what) => {
73 if (!p) throw new Error(`CKey: ${lastError(`${what} failed`)}`);
74 const n = M._ckw_last_len();
75 const out = heap().slice(p, p + n);
76 M._ckw_free(p);
77 return out;
78 };
79 const u32s = (p, n) => Array.from(new Uint32Array(heap().buffer, p, n));
80 const f64s = (p, n) => Array.from(new Float64Array(heap().buffer, p, n));
81
82 // ---- Safe Actions (verification is the core's) -----------------------------
83 const safeActionCall = (bytes, key, nowSeconds, maxLifetime) => {
84 if (typeof M._ckw_safe_action !== 'function') throw new Error('this CKey core has no Safe Actions');
85 const p = copyIn(bytes);
86 const k = key ? copyIn(key) : 0;
87 try {
88 const out = M._ckw_safe_action(p, bytes.length, k, nowSeconds, maxLifetime);
89 if (!out) throw new Error(lastError('not a valid Safe Action'));
90 const fields = JSON.parse(cstring(out));
91 M._ckw_free(out);
92 fields.scope = fields.scope === 1 ? 'cross-device' : 'same-device';
93 fields.params = Uint8Array.from((fields.params.match(/../g) || []).map((b) => parseInt(b, 16)));
94 return fields;
95 } finally {
96 M._ckw_free(p);
97 if (k) M._ckw_free(k);
98 }
99 };
100 const safeActionCore = {
101 parse: (bytes) => safeActionCall(bytes, null, 0, 0),
102 verify: (bytes, key, nowSeconds, maxLifetime) => safeActionCall(bytes, key, nowSeconds, maxLifetime),
103 };
104
105 // ---- symbols -------------------------------------------------------------
106 const registry =
107 typeof FinalizationRegistry === 'function' ? new FinalizationRegistry((ptr) => M._ckw_symbol_free(ptr)) : null;
108
109 class CKeySymbol {
110 #ptr;
111 constructor(ptr) {
112 this.#ptr = ptr;
113 const info = alloc(20);
114 try {
115 M._ckw_symbol_info(ptr, info);
116 const [version, level, layout, payloadLength] = u32s(info, 5);
117 Object.defineProperties(this, {
118 version: { value: version, enumerable: true },
119 level: { value: LEVELS[level], enumerable: true },
120 layout: { value: LAYOUTS[layout], enumerable: true },
121 payloadLength: { value: payloadLength, enumerable: true },
122 capacity: { value: M._ckw_scalable_capacity(version, level, layout), enumerable: true },
123 modules: { value: M._ckw_scalable_modules_across(version), enumerable: true },
124 });
125 } finally {
126 M._ckw_free(info);
127 }
128 if (registry) registry.register(this, ptr, this);
129 }
130 /** @internal */
131 static ptrOf(sym) {
132 if (!(sym instanceof CKeySymbol)) throw new TypeError('expected a symbol from this CKey instance (ckey.encode)');
133 if (!sym.#ptr) throw new Error('CKey: this symbol has been freed');
134 return sym.#ptr;
135 }
136 get freed() {
137 return !this.#ptr;
138 }
139 /** Releases the symbol's WebAssembly memory. Safe to call twice. */
140 free() {
141 if (!this.#ptr) return;
142 if (registry) registry.unregister(this);
143 M._ckw_symbol_free(this.#ptr);
144 this.#ptr = 0;
145 }
146 }
147
148 // ---- styles --------------------------------------------------------------
149 /** Runs fn(stylePtr) with a core style built from a styleSpec (0 = default). */
150 const withStyle = (spec, fn) => {
151 if (!spec) return fn(0);
152 const ws = M._ckw_style_new(spec.preset);
153 if (!ws) throw new Error(`CKey: ${lastError('invalid style preset')}`);
154 try {
155 for (const [field, r, g, b] of spec.rgb) {
156 if (!M._ckw_style_set_rgb(ws, field, r, g, b)) throw new RangeError('CKey: invalid style colour');
157 }
158 for (const [field, v] of spec.num) {
159 if (!M._ckw_style_set_num(ws, field, v)) {
160 throw new RangeError(
161 field === 10 ? 'CKey: this core build supports only the strict style policy' : 'CKey: invalid style value',
162 );
163 }
164 }
165 if (spec.logoHref !== null) withCString(spec.logoHref, (p) => M._ckw_style_set_logo(ws, p));
166 return fn(ws);
167 } finally {
168 M._ckw_style_free(ws);
169 }
170 };
171
172 // ---- rendering -----------------------------------------------------------
173 const rasterRGBA = (sym, o) => {
174 const ptr = CKeySymbol.ptrOf(sym);
175 return withStyle(o.style, (ws) => {
176 let logoPtr = 0;
177 try {
178 if (o.logo) logoPtr = copyIn(o.logo.data.subarray(0, o.logo.width * o.logo.height * 4));
179 const p = M._ckw_render_raster(
180 ptr, ws, o.size, o.policy, o.transparent ? 1 : 0, o.supersample, o.minFeaturePx, o.materials ? 1 : 0, 0,
181 logoPtr, o.logo ? o.logo.width : 0, o.logo ? o.logo.height : 0,
182 );
183 return takeOut(p, 'render');
184 } finally {
185 if (logoPtr) M._ckw_free(logoPtr);
186 }
187 });
188 };
189
190 function renderPixels(sym, opts) {
191 const o = rasterOptions(opts);
192 const px = rasterRGBA(sym, o);
193 return { width: o.size, height: o.size, data: new Uint8ClampedArray(px.buffer, px.byteOffset, px.byteLength) };
194 }
195
196 function renderToCanvas(sym, canvas, opts) {
197 if (!canvas || typeof canvas.getContext !== 'function') {
198 throw new TypeError('renderToCanvas needs an HTMLCanvasElement or OffscreenCanvas');
199 }
200 const img = renderPixels(sym, opts);
201 if (canvas.width !== img.width) canvas.width = img.width;
202 if (canvas.height !== img.height) canvas.height = img.height;
203 const ctx = canvas.getContext('2d');
204 if (!ctx) throw new Error('CKey: could not get a 2d context from the canvas');
205 let data;
206 if (typeof ImageData === 'function') {
207 data = new ImageData(img.data, img.width, img.height);
208 } else {
209 data = ctx.createImageData(img.width, img.height);
210 data.data.set(img.data);
211 }
212 ctx.putImageData(data, 0, 0);
213 return canvas;
214 }
215
216 function renderPNG(sym, opts) {
217 const o = rasterOptions(opts);
218 const px = rasterRGBA(sym, o);
219 const p = copyIn(px);
220 try {
221 return takeOut(M._ckw_encode_png_rgba(p, o.size, o.size, o.dpi), 'PNG export');
222 } finally {
223 M._ckw_free(p);
224 }
225 }
226
227 function renderSVG(sym, opts) {
228 const o = svgOptions(opts);
229 const ptr = CKeySymbol.ptrOf(sym);
230 return withStyle(o.style, (ws) => utf8d.decode(takeOut(M._ckw_render_svg(ptr, ws, o.scale), 'SVG export')));
231 }
232
233 function renderPDF(sym, opts) {
234 const o = pdfOptions(opts);
235 const ptr = CKeySymbol.ptrOf(sym);
236 return withStyle(o.style, (ws) => takeOut(M._ckw_render_pdf(ptr, ws, o.widthPt, o.transparent ? 1 : 0), 'PDF export'));
237 }
238
239 /** Checks a style against a symbol: { ok, message } (the core's explanation). */
240 function checkStyle(sym, opts) {
241 const o = rasterOptions({ preset: opts?.preset, style: opts?.style });
242 const ptr = CKeySymbol.ptrOf(sym);
243 return withStyle(o.style, (ws) => {
244 const st = M._ckw_style_check(ptr, ws);
245 return { ok: st === 0, message: lastError(st === 0 ? 'style OK' : 'style refused') };
246 });
247 }
248
249 /** Optical certification through the core's modelled scan (ABI 4 cores). */
250 function certify(sym, opts) {
251 if (!M._ckw_has_certify || !M._ckw_has_certify()) throw new Error('CKey: this core build has no certification');
252 const o = certifyOptions(opts);
253 const ptr = CKeySymbol.ptrOf(sym);
254 return withStyle(o.style, (ws) => {
255 const colours = o.colours.length ? copyIn(Uint8Array.from(o.colours.flat())) : 0;
256 const pixels = o.pixels ? copyIn(o.pixels.data.subarray(0, o.pixels.width * o.pixels.height * 4)) : 0;
257 const out = alloc(64);
258 try {
259 const st = M._ckw_certify(
260 ptr, ws, o.transparent ? 1 : 0, colours, o.colours.length, pixels,
261 o.pixels ? o.pixels.width : 0, o.pixels ? o.pixels.height : 0,
262 o.pxPerModule, o.blurPx, o.noise, o.glare, o.trials, out, 0,
263 );
264 const message = lastError('');
265 const [certified, policy, adjustments, trials, trialsPassed, minContrast, featureSeparation, logoLoad] = f64s(out, 8);
266 return {
267 ok: st === 0,
268 certified: certified === 1,
269 policy: STYLE_POLICY_NAMES[policy] ?? String(policy),
270 adjustments: ADJUSTMENTS.filter((_, i) => adjustments & (1 << i)),
271 trials,
272 trialsPassed,
273 minContrast,
274 featureSeparation,
275 logoLoad,
276 message,
277 };
278 } finally {
279 M._ckw_free(out);
280 if (colours) M._ckw_free(colours);
281 if (pixels) M._ckw_free(pixels);
282 }
283 });
284 }
285
286 // ---- scanning ------------------------------------------------------------
287 let frameBuf = 0;
288 let frameCap = 0;
289 const frameScratch = (n) => {
290 if (n > frameCap) {
291 if (frameBuf) M._ckw_free(frameBuf);
292 frameBuf = 0;
293 frameCap = 0;
294 frameBuf = alloc(n);
295 frameCap = n;
296 }
297 return frameBuf;
298 };
299
300 const decodeWith = (fnName, f, channels) => {
301 const n = f.width * f.height * channels;
302 const p = frameScratch(n);
303 heap().set(f.data.subarray(0, n), p);
304 const info = alloc(16);
305 try {
306 const out = M[fnName](p, f.width, f.height, 0, info);
307 if (!out) return null;
308 const bytes = takeOut(out, 'decode');
309 const [code, version, layout] = u32s(info, 3);
310 return {
311 bytes,
312 text: readableText(bytes),
313 profile: PROFILES[code],
314 version: code <= 1 ? version : null,
315 layout: code <= 1 ? LAYOUTS[layout] : null,
316 profileCode: code,
317 };
318 } finally {
319 M._ckw_free(info);
320 }
321 };
322 const strip = (r) => {
323 if (!r) return null;
324 const { profileCode, ...rest } = r; // eslint-disable-line no-unused-vars
325 return rest;
326 };
327 const decodeRGBA = (img) => decodeWith('_ckw_decode_rgba', rgbaFrame(img), 4);
328
329 // ---- telemetry -----------------------------------------------------------
330 const hasTelemetry = typeof M._ckw_tel_set_mode === 'function';
331 const telCore = hasTelemetry && {
332 shouldSend: (t) => M._ckw_tel_should_send(t) !== 0,
333 takeBatch: (day) =>
334 withCString(day, (d) => {
335 const p = M._ckw_tel_take_batch(d);
336 if (!p) return null;
337 const id = M._ckw_tel_last_batch_id();
338 return { json: utf8d.decode(takeOut(p, 'telemetry batch')), id };
339 }),
340 batchResult: (id, status, t) => M._ckw_tel_batch_result(id, status, t) === 0,
341 };
342 const recorder = hasTelemetry
343 ? {
344 begin: (t) => M._ckw_tel_session_begin(t),
345 frame: (t, ms) => M._ckw_tel_frame(t, ms),
346 success: (t, code) => M._ckw_tel_session_success(t, code),
347 end: (t) => M._ckw_tel_session_end(t),
348 }
349 : { begin() {}, frame() {}, success() {}, end() {} };
350
351 const version = cstring(M._ckw_software_version());
352 let sender = null;
353 if (hasTelemetry) {
354 withCString('web', (sdk) =>
355 withCString(version, (v) => withCString('web', (platform) => M._ckw_tel_set_client(sdk, v, platform))),
356 );
357 sender = new TelemetrySender({
358 core: telCore,
359 fetch: options.fetch === undefined ? undefined : options.fetch,
360 endpoint: options.telemetryEndpoint,
361 now,
362 day: options.day || (() => utcDay()),
363 });
364 installBackgroundSend(sender, options.document ?? globalThis.document, options.window ?? globalThis.window);
365 }
366
367 const setTelemetry = (v) => {
368 const mode = telemetryMode(v);
369 if (!hasTelemetry) {
370 if (mode !== 0) throw new RangeError('this CKey build has no telemetry module; only 0 is accepted');
371 return;
372 }
373 if (M._ckw_tel_set_mode(mode) !== 0) throw new RangeError(`telemetry must be 0, 1 or 2, got ${v}`);
374 };
375 if (options.telemetry !== undefined) setTelemetry(options.telemetry);
376
377 const ckey = {
378 version,
379 abiVersion: M._ckw_abi_version(),
380
381 encode(input, opts) {
382 const bytes = payloadBytes(input);
383 const o = encodeOptions(opts);
384 const p = copyIn(bytes);
385 try {
386 const sym = M._ckw_encode(p, bytes.length, o.version, o.level, o.layout);
387 if (!sym) throw new Error(lastError('encode failed'));
388 return new CKeySymbol(sym);
389 } finally {
390 M._ckw_free(p);
391 }
392 },
393 capacity(version, level = 'M', layout = 'standard') {
394 const o = encodeOptions({ version, level, layout });
395 return M._ckw_scalable_capacity(o.version, o.level, o.layout);
396 },
397
398 renderToCanvas,
399 renderPixels,
400 renderPNG,
401 renderSVG,
402 renderPDF,
403 checkStyle,
404 certify,
405
406 decodeImageData: (img) => strip(decodeRGBA(img)),
407 decodeGray: (img) => strip(decodeWith('_ckw_decode_gray', grayFrame(img), 1)),
408 classifyPayload,
409
410 createScanner({ onResult, onError } = {}) {
411 return makeScanner({ decode: decodeRGBA, telemetry: recorder, now, sender }, { onResult, onError });
412 },
413
414 get telemetry() {
415 return hasTelemetry ? M._ckw_tel_get_mode() : 0;
416 },
417 set telemetry(v) {
418 setTelemetry(v);
419 },
420 /** Local aggregate counters (modes 1 and 2; all zero in mode 0). */
421 telemetryCounters() {
422 if (!hasTelemetry) return null;
423 const p = alloc(33 * 8);
424 try {
425 M._ckw_tel_snapshot(p);
426 const v = f64s(p, 33);
427 const hist = (o) => ({ count: v[o], sum: v[o + 1], buckets: v.slice(o + 2, o + 12) });
428 return {
429 sessions: { started: v[0], succeeded: v[1], failed: v[2] },
430 frames: v[3],
431 decoded: { p0: v[4], p0_1: v[5], p1: v[6], p2: v[7], p3: v[8] },
432 time_to_decode_ms: hist(9),
433 frame_ms: hist(21),
434 };
435 } finally {
436 M._ckw_free(p);
437 }
438 },
439 /**
440 * Safe Actions for this app (spec/CKey-Safe-Actions.md): pin issuer keys with
441 * trust(), register actions with register(), and pass decoded text to
442 * handle(). Same-device actions run at once; cross-device ones only after
443 * request.present() and the person's approval.
444 */
445 safeActions(appId, safeActionOptions) {
446 return new SafeActions(safeActionCore, appId, safeActionOptions);
447 },
448 /** Attempts a telemetry send now if one is due (mode 1). Never rejects. */
449 flushTelemetry() {
450 return sender ? sender.trySend() : Promise.resolve(null);
451 },
452 };
453 Object.defineProperty(ckey, '_testing', {
454 value: {
455 sender,
456 resetTelemetry: () => hasTelemetry && M._ckw_tel_reset_for_testing(),
457 setClient: () =>
458 withCString('web', (sdk) =>
459 withCString(version, (v) => withCString('web', (pl) => M._ckw_tel_set_client(sdk, v, pl))),
460 ),
461 },
462 });
463 return Object.seal(ckey);
464}