@wtfalch/auth 0.19.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,404 @@
1
+ /**
2
+ * "The code builds the door": the QR sign-in code, and the short animation it
3
+ * plays once the phone approves. Framework-free canvas drawing; `./qr-door`
4
+ * wraps it in a React component.
5
+ *
6
+ * At rest it is an ordinary, scannable QR code for the device grant's `url`
7
+ * with a small clearing in the middle. On approval, modules lift out of the
8
+ * code and land on the outline of the app's mark, the solid line traces in
9
+ * behind them, a person (the mark's red dot) steps into the doorway, and one
10
+ * perspective camera move carries you through it to the app's icon.
11
+ *
12
+ * Everything is drawn in a fixed 360×360 logical space; the caller sets the
13
+ * canvas transform for its size and devicePixelRatio. `buildQrDoorScene` does
14
+ * the one-off work (QR matrix, which module flies where, the camera solve) and
15
+ * is plain data, safe on a server. `drawQrDoorRest` and `drawQrDoorScene` need
16
+ * a canvas and are a pure function of (scene, t): scrub, test or freeze them.
17
+ *
18
+ * Browser file for drawing: nothing here touches OIDC, cookies or `node:`.
19
+ */
20
+ import QRCode from 'qrcode';
21
+ const MARKS = {
22
+ agora: {
23
+ d: 'M5.6 18.2V12.6a6.4 6.4 0 0 1 12.8 0V18.2',
24
+ dot: [12, 15.1],
25
+ len: 31.3,
26
+ hc: [12, 13.4],
27
+ hr: 5.2,
28
+ segs: [
29
+ ['L', 5.6, 18.2, 5.6, 12.6],
30
+ ['A', 12, 12.6, 6.4, 180, 360],
31
+ ['L', 18.4, 12.6, 18.4, 18.2],
32
+ ],
33
+ hole: 'M7.3 18.2V12.6a4.7 4.7 0 0 1 9.4 0V18.2Z',
34
+ },
35
+ boule: {
36
+ d: 'M8.6 17.89A6.8 6.8 0 1 1 15.4 17.89',
37
+ dot: [12, 12],
38
+ len: 35.6,
39
+ hc: [12, 12],
40
+ hr: 5.1,
41
+ segs: [['A', 12, 12, 6.8, 120, 420]],
42
+ hole: 'M6.9 12a5.1 5.1 0 1 0 10.2 0a5.1 5.1 0 1 0 -10.2 0Z',
43
+ },
44
+ archon: {
45
+ d: 'M9.3 19.28A5.4 5.4 0 1 1 14.7 19.28',
46
+ dot: [12, 4.3],
47
+ len: 28.3,
48
+ hc: [12, 14.6],
49
+ hr: 3.7,
50
+ segs: [['A', 12, 14.6, 5.4, 120, 420]],
51
+ hole: 'M8.3 14.6a3.7 3.7 0 1 0 7.4 0a3.7 3.7 0 1 0 -7.4 0Z',
52
+ },
53
+ };
54
+ export const QR_DOOR_THEME = { ink: '#1D1B16', accent: '#B23A0C', paper: '#F6F3EC' };
55
+ /* ---------- geometry (logical px) ---------- */
56
+ /** Side of the logical drawing space. */
57
+ export const QR_DOOR_SIZE = 360;
58
+ const L = QR_DOOR_SIZE;
59
+ const C0 = L / 2;
60
+ const CODE = 290;
61
+ const O = (L - CODE) / 2;
62
+ const ICON = 228;
63
+ const TAU = Math.PI * 2;
64
+ /** Icon units to px for the doorway drawn in the code. */
65
+ const S1 = 2.9;
66
+ /** Half-width of the clearing the doorway sits in. */
67
+ export const QR_DOOR_CLEARING = 25;
68
+ /** Icon size when first seen through the doorway, and the instant the doorway clears the stage. */
69
+ const START = 9;
70
+ const S0 = 88;
71
+ /** Finder-eye corner rounding. Above about 0.5 strict decoders stop finding the eyes. */
72
+ const EYE_ROUND = 0.4;
73
+ /** Largest QR version allowed: version 7 and up put an alignment pattern dead centre, under the door. */
74
+ export const QR_DOOR_MAX_VERSION = 6;
75
+ /* ---------- timeline (seconds after approval) ---------- */
76
+ const WALK = 0.95;
77
+ const DOLLY = 1.55;
78
+ /** Length of the approval animation. */
79
+ export const QR_DOOR_DURATION = WALK + DOLLY + 0.3;
80
+ /* ---------- easing ---------- */
81
+ const clamp = (v, a = 0, b = 1) => (v < a ? a : v > b ? b : v);
82
+ const seg = (u, a, b) => clamp((u - a) / (b - a));
83
+ const lerp = (a, b, t) => a + (b - a) * t;
84
+ const io = (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
85
+ const sine = (t) => -(Math.cos(Math.PI * t) - 1) / 2;
86
+ const back = (t) => 1 + 2.5 * (t - 1) ** 3 + 1.5 * (t - 1) ** 2;
87
+ /**
88
+ * Builds the scene for `url` (the device grant's verification URL). Tries
89
+ * error correction Q, then M, then L, keeping the first that fits in version 6.
90
+ * The clearing knocks out about 3% of modules, well inside even L's 7%.
91
+ * Throws a `RangeError` for a URL too long for version 6 at any level.
92
+ */
93
+ export function buildQrDoorScene(url, mark) {
94
+ const geo = MARKS[mark];
95
+ let qr = null;
96
+ let errorCorrection = 'Q';
97
+ for (const level of ['Q', 'M', 'L']) {
98
+ const candidate = QRCode.create(url, { errorCorrectionLevel: level });
99
+ if (candidate.version <= QR_DOOR_MAX_VERSION) {
100
+ qr = candidate;
101
+ errorCorrection = level;
102
+ break;
103
+ }
104
+ }
105
+ if (!qr) {
106
+ throw new RangeError(`qr-door: ${url.length} characters needs a QR version above ${QR_DOOR_MAX_VERSION}`);
107
+ }
108
+ const n = qr.modules.size;
109
+ const data = qr.modules.data;
110
+ const m = CODE / n;
111
+ const isFinder = (x, y) => (x < 7 && y < 7) || (x > n - 8 && y < 7) || (x < 7 && y > n - 8);
112
+ const dots = [];
113
+ for (let y = 0; y < n; y++) {
114
+ for (let x = 0; x < n; x++) {
115
+ if (!data[y * n + x] || isFinder(x, y))
116
+ continue;
117
+ const px = O + (x + 0.5) * m;
118
+ const py = O + (y + 0.5) * m;
119
+ if (Math.abs(px - C0) < QR_DOOR_CLEARING && Math.abs(py - C0) < QR_DOOR_CLEARING)
120
+ continue;
121
+ dots.push({ x: px, y: py });
122
+ }
123
+ }
124
+ const finders = [
125
+ [0, 0],
126
+ [n - 7, 0],
127
+ [0, n - 7],
128
+ ].map(([fx, fy]) => ({ x: O + (fx + 3.5) * m, y: O + (fy + 3.5) * m }));
129
+ // Sample the outline evenly and give each sample the nearest module not yet taken.
130
+ const poly = [];
131
+ for (const s of geo.segs) {
132
+ if (s[0] === 'L') {
133
+ for (let i = 0; i <= 30; i++)
134
+ poly.push([lerp(s[1], s[3], i / 30), lerp(s[2], s[4], i / 30)]);
135
+ }
136
+ else {
137
+ for (let i = 0; i <= 120; i++) {
138
+ const a = ((s[4] + ((s[5] - s[4]) * i) / 120) * Math.PI) / 180;
139
+ poly.push([s[1] + s[3] * Math.cos(a), s[2] + s[3] * Math.sin(a)]);
140
+ }
141
+ }
142
+ }
143
+ const cum = [0];
144
+ for (let i = 1; i < poly.length; i++)
145
+ cum.push(cum[i - 1] + Math.hypot(poly[i][0] - poly[i - 1][0], poly[i][1] - poly[i - 1][1]));
146
+ const len = cum[cum.length - 1];
147
+ const count = Math.ceil((len * S1) / 3.6);
148
+ const used = new Set();
149
+ const pieces = [];
150
+ for (let j = 0; j < count; j++) {
151
+ const l = (j / (count - 1)) * len;
152
+ let q = 1;
153
+ while (q < cum.length - 1 && cum[q] < l)
154
+ q++;
155
+ const a = poly[q - 1];
156
+ const b = poly[q];
157
+ const f = clamp((l - cum[q - 1]) / (cum[q] - cum[q - 1] || 1));
158
+ const tx = C0 + (lerp(a[0], b[0], f) - geo.hc[0]) * S1;
159
+ const ty = C0 + (lerp(a[1], b[1], f) - geo.hc[1]) * S1;
160
+ let best = -1;
161
+ let bd = Number.POSITIVE_INFINITY;
162
+ for (let d = 0; d < dots.length; d++) {
163
+ if (used.has(d))
164
+ continue;
165
+ const dd = Math.hypot(dots[d].x - tx, dots[d].y - ty);
166
+ if (dd < bd) {
167
+ bd = dd;
168
+ best = d;
169
+ }
170
+ }
171
+ used.add(best);
172
+ pieces.push({ src: best, sx: dots[best].x, sy: dots[best].y, tx, ty, dl: (j / count) * 0.32 });
173
+ }
174
+ // One perspective camera: the code is a plane at depth 1, the icon at depth Dp.
175
+ // Solved so the icon is START px when first seen, S0 px the instant the doorway
176
+ // fills the stage, and ICON px at rest -- no hand-off between two zooms.
177
+ const Zg = (1.06 * 255) / (geo.hr * S1);
178
+ const zg = 1 - 1 / Zg;
179
+ const Dp = (zg * S0) / (S0 - START);
180
+ const kk = START * Dp;
181
+ const zf = Dp - kk / ICON;
182
+ let lo = 0;
183
+ let hi = 1;
184
+ for (let i = 0; i < 30; i++) {
185
+ const mid = (lo + hi) / 2;
186
+ if (io(mid) * zf < zg)
187
+ lo = mid;
188
+ else
189
+ hi = mid;
190
+ }
191
+ return {
192
+ mark,
193
+ errorCorrection,
194
+ version: qr.version,
195
+ modules: n,
196
+ moduleSize: m,
197
+ dots,
198
+ finders,
199
+ pieces,
200
+ camera: { Zg, zg, Dp, kk, zf, t1: WALK + DOLLY * hi },
201
+ };
202
+ }
203
+ /** Icon size (logical px) at time `t` after approval. Continuous by construction. */
204
+ export function qrDoorIconSize(scene, t) {
205
+ const c = scene.camera;
206
+ const z = c.zf * io(seg(t, WALK, WALK + DOLLY));
207
+ return c.kk / (c.Dp - z);
208
+ }
209
+ const paths = new Map();
210
+ function pathsFor(mark) {
211
+ let p = paths.get(mark);
212
+ if (!p) {
213
+ p = { stroke: new Path2D(MARKS[mark].d), hole: new Path2D(MARKS[mark].hole) };
214
+ paths.set(mark, p);
215
+ }
216
+ return p;
217
+ }
218
+ function rr(c, x, y, w, h, r) {
219
+ c.moveTo(x + r, y);
220
+ c.arcTo(x + w, y, x + w, y + h, r);
221
+ c.arcTo(x + w, y + h, x, y + h, r);
222
+ c.arcTo(x, y + h, x, y, r);
223
+ c.arcTo(x, y, x + w, y, r);
224
+ c.closePath();
225
+ }
226
+ function finder(c, s, p, th) {
227
+ const M = s.moduleSize;
228
+ const h = 3.5 * M;
229
+ c.save();
230
+ c.translate(p.x - h, p.y - h);
231
+ c.fillStyle = th.ink;
232
+ c.beginPath();
233
+ rr(c, 0, 0, 7 * M, 7 * M, 1.6 * M * EYE_ROUND);
234
+ rr(c, M, M, 5 * M, 5 * M, 0.8 * M * EYE_ROUND);
235
+ c.fill('evenodd');
236
+ c.beginPath();
237
+ rr(c, 2 * M, 2 * M, 3 * M, 3 * M, 0.75 * M * EYE_ROUND);
238
+ c.fill();
239
+ c.restore();
240
+ }
241
+ function circles(c, xyr, color) {
242
+ c.fillStyle = color;
243
+ c.beginPath();
244
+ for (let i = 0; i < xyr.length; i += 3) {
245
+ const r = xyr[i + 2];
246
+ if (r <= 0.1)
247
+ continue;
248
+ c.moveTo(xyr[i] + r, xyr[i + 1]);
249
+ c.arc(xyr[i], xyr[i + 1], r, 0, TAU);
250
+ }
251
+ c.fill();
252
+ }
253
+ function stroke(c, s, th, dash) {
254
+ c.lineWidth = 3.4;
255
+ c.lineCap = 'round';
256
+ c.strokeStyle = th.ink;
257
+ if (dash)
258
+ c.setLineDash(dash);
259
+ c.stroke(pathsFor(s.mark).stroke);
260
+ c.setLineDash([]);
261
+ }
262
+ function icon(c, s, th, size) {
263
+ const k = size / 24;
264
+ c.save();
265
+ c.translate(C0 - size / 2, C0 - size / 2);
266
+ c.scale(k, k);
267
+ c.save();
268
+ c.shadowColor = 'rgba(29,27,22,.16)';
269
+ c.shadowBlur = 18 / k;
270
+ c.shadowOffsetY = 6 / k;
271
+ c.beginPath();
272
+ rr(c, 0.25, 0.25, 23.5, 23.5, 5.4);
273
+ c.fillStyle = th.paper;
274
+ c.fill();
275
+ c.shadowColor = 'transparent';
276
+ c.lineWidth = 0.2;
277
+ c.strokeStyle = 'rgba(29,27,22,.14)';
278
+ c.stroke();
279
+ c.restore();
280
+ stroke(c, s, th);
281
+ c.restore();
282
+ }
283
+ /** The code at rest, waiting for the phone. */
284
+ export function drawQrDoorRest(c, s, th = QR_DOOR_THEME) {
285
+ c.fillStyle = th.paper;
286
+ c.fillRect(0, 0, L, L);
287
+ const r = 0.42 * s.moduleSize;
288
+ const a = [];
289
+ for (const p of s.dots)
290
+ a.push(p.x, p.y, r);
291
+ circles(c, a, th.ink);
292
+ for (const f of s.finders)
293
+ finder(c, s, f, th);
294
+ }
295
+ /** The frame `t` seconds after approval; `t` is clamped to `[0, QR_DOOR_DURATION]`. */
296
+ export function drawQrDoorScene(c, s, time, th = QR_DOOR_THEME) {
297
+ const t = clamp(time, 0, QR_DOOR_DURATION);
298
+ const geo = MARKS[s.mark];
299
+ const cam = s.camera;
300
+ const hc = geo.hc;
301
+ const R0 = 0.42 * s.moduleSize;
302
+ const halfLine = 1.7 * S1;
303
+ const { hole: holePath } = pathsFor(s.mark);
304
+ // The opening: modules fly onto the outline; the line traces in behind the landings.
305
+ // Each lands at exactly half the line's width, so there is no cross-fade to see.
306
+ const trace = clamp((t - 0.345) / 0.32);
307
+ const hole = io(seg(t, 0.55, 0.8));
308
+ const arrive = back(seg(t, 0.72, 0.95));
309
+ const lifted = new Set();
310
+ for (const p of s.pieces)
311
+ if (t > p.dl)
312
+ lifted.add(p.src);
313
+ // The walk: one camera for both planes.
314
+ const z = cam.zf * io(seg(t, WALK, WALK + DOLLY));
315
+ const gone = z >= cam.zg;
316
+ const zoom = gone ? cam.Zg : 1 / (1 - z);
317
+ const isz = cam.kk / (cam.Dp - z);
318
+ const k = isz / ICON;
319
+ // Far side: dotted ground and the icon, visible through the doorway from the start of the walk.
320
+ c.fillStyle = th.paper;
321
+ c.fillRect(0, 0, L, L);
322
+ const sp = Math.max(22 * k, 4.5);
323
+ const dr = sp * 0.05;
324
+ const g0 = C0 - Math.ceil(C0 / sp) * sp;
325
+ c.fillStyle = 'rgba(29,27,22,.13)';
326
+ c.beginPath();
327
+ for (let gx = g0; gx < L + sp; gx += sp) {
328
+ for (let gy = g0; gy < L + sp; gy += sp) {
329
+ c.moveTo(gx + dr, gy);
330
+ c.arc(gx, gy, dr, 0, TAU);
331
+ }
332
+ }
333
+ c.fill();
334
+ const iconAlpha = sine(seg(t, WALK - 0.1, WALK + 0.35));
335
+ if (iconAlpha > 0) {
336
+ c.save();
337
+ c.globalAlpha = iconAlpha;
338
+ icon(c, s, th, isz);
339
+ c.restore();
340
+ }
341
+ // The person: from the doorway to the icon's own dot, then rides with it.
342
+ const w = io(seg(t, WALK, cam.t1));
343
+ const px = lerp(C0 + (geo.dot[0] - hc[0]) * S1, C0 + ((geo.dot[0] - 12) * isz) / 24, w);
344
+ const py = lerp(C0 + (geo.dot[1] - hc[1]) * S1, C0 + ((geo.dot[1] - 12) * isz) / 24, w);
345
+ const pr = lerp(2.6 * S1 * arrive, (2.6 * isz) / 24, w);
346
+ // Near side: the code, the doorway cut out of it, rushing past the camera.
347
+ if (!gone) {
348
+ c.save();
349
+ c.translate(C0, C0);
350
+ c.scale(zoom, zoom);
351
+ c.translate(-C0, -C0);
352
+ c.save();
353
+ const clip = new Path2D();
354
+ clip.rect(-4000, -4000, 8000 + L, 8000 + L);
355
+ if (hole > 0) {
356
+ clip.addPath(holePath, new DOMMatrix()
357
+ .translate(C0 - hc[0] * S1, C0 - hc[1] * S1)
358
+ .scale(S1)
359
+ .translate(hc[0], hc[1])
360
+ .scale(hole)
361
+ .translate(-hc[0], -hc[1]));
362
+ }
363
+ c.clip(clip, 'evenodd');
364
+ c.fillStyle = th.paper;
365
+ c.fillRect(-4000, -4000, 8000 + L, 8000 + L);
366
+ const rest = [];
367
+ s.dots.forEach((p, i) => {
368
+ if (!lifted.has(i))
369
+ rest.push(p.x, p.y, R0);
370
+ });
371
+ circles(c, rest, th.ink);
372
+ for (const f of s.finders)
373
+ finder(c, s, f, th);
374
+ c.restore();
375
+ if (trace < 1) {
376
+ const fly = [];
377
+ for (const p of s.pieces) {
378
+ if (t <= p.dl)
379
+ continue;
380
+ const e = io(seg(t, p.dl, p.dl + 0.32));
381
+ const lift = Math.sin(Math.PI * e) * 10;
382
+ fly.push(lerp(p.sx, p.tx, e), lerp(p.sy, p.ty, e) - lift, lerp(R0, halfLine, e));
383
+ }
384
+ circles(c, fly, th.ink);
385
+ }
386
+ c.restore();
387
+ }
388
+ if (pr > 0.1) {
389
+ c.fillStyle = th.accent;
390
+ c.beginPath();
391
+ c.arc(px, py, pr, 0, TAU);
392
+ c.fill();
393
+ }
394
+ if (!gone && trace > 0) {
395
+ c.save();
396
+ c.translate(C0, C0);
397
+ c.scale(zoom, zoom);
398
+ c.translate(-C0, -C0);
399
+ c.translate(C0 - hc[0] * S1, C0 - hc[1] * S1);
400
+ c.scale(S1, S1);
401
+ stroke(c, s, th, trace < 1 ? [geo.len * trace, geo.len * 2] : undefined);
402
+ c.restore();
403
+ }
404
+ }
@@ -0,0 +1,15 @@
1
+ import { type QrDoorMark, type QrDoorTheme } from './qr-door-scene.js';
2
+ export { QR_DOOR_DURATION, QR_DOOR_THEME, type QrDoorMark, type QrDoorScene, type QrDoorTheme, buildQrDoorScene, drawQrDoorRest, drawQrDoorScene, } from './qr-door-scene.js';
3
+ export interface QrDoorProps {
4
+ /** What the phone scans: `startQrSignIn()`'s `url`. */
5
+ url: string;
6
+ /** Whose door: the app's mark. */
7
+ mark: QrDoorMark;
8
+ /** True once `pollQrSignIn()` answered `signed_in`. */
9
+ approved: boolean;
10
+ /** Called when the walk-through ends, or at once under reduced motion. Navigate here. */
11
+ onDone?: () => void;
12
+ theme?: Partial<QrDoorTheme>;
13
+ className?: string;
14
+ }
15
+ export declare function QrDoor({ url, mark, approved, onDone, theme, className }: QrDoorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * `<QrDoor>`: the QR sign-in code as a React component. Draws `url` (the
5
+ * `url` from `startQrSignIn()`) and plays the door animation once `approved`
6
+ * turns true -- that is, once `pollQrSignIn()` answered `signed_in`. The
7
+ * drawing itself lives in `qr-door-scene.ts`; this file only owns the canvas,
8
+ * its size, the animation clock and reduced motion.
9
+ *
10
+ * Browser file: nothing here touches OIDC, cookies or `node:`.
11
+ */
12
+ import { useEffect, useMemo, useRef } from 'react';
13
+ import { QR_DOOR_DURATION, QR_DOOR_SIZE, QR_DOOR_THEME, buildQrDoorScene, drawQrDoorRest, drawQrDoorScene, } from './qr-door-scene.js';
14
+ export { QR_DOOR_DURATION, QR_DOOR_THEME, buildQrDoorScene, drawQrDoorRest, drawQrDoorScene, } from './qr-door-scene.js';
15
+ export function QrDoor({ url, mark, approved, onDone, theme, className }) {
16
+ const canvasRef = useRef(null);
17
+ const scene = useMemo(() => buildQrDoorScene(url, mark), [url, mark]);
18
+ const ink = theme?.ink ?? QR_DOOR_THEME.ink;
19
+ const accent = theme?.accent ?? QR_DOOR_THEME.accent;
20
+ const paper = theme?.paper ?? QR_DOOR_THEME.paper;
21
+ const onDoneRef = useRef(onDone);
22
+ onDoneRef.current = onDone;
23
+ useEffect(() => {
24
+ const canvas = canvasRef.current;
25
+ const ctx = canvas?.getContext('2d');
26
+ if (!canvas || !ctx)
27
+ return;
28
+ const th = { ink, accent, paper };
29
+ const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
30
+ const start = performance.now();
31
+ let raf = 0;
32
+ let finished = false;
33
+ const fit = () => {
34
+ const dpr = Math.min(window.devicePixelRatio || 1, 2.5);
35
+ const px = Math.round((canvas.clientWidth || QR_DOOR_SIZE) * dpr);
36
+ // A canvas starts at 300×150, so check both sides, not just the width.
37
+ if (canvas.width !== px || canvas.height !== px)
38
+ canvas.width = canvas.height = px;
39
+ ctx.setTransform(px / QR_DOOR_SIZE, 0, 0, px / QR_DOOR_SIZE, 0, 0);
40
+ };
41
+ const paint = () => {
42
+ fit();
43
+ if (!approved) {
44
+ drawQrDoorRest(ctx, scene, th);
45
+ return;
46
+ }
47
+ const t = reduce ? QR_DOOR_DURATION : (performance.now() - start) / 1000;
48
+ drawQrDoorScene(ctx, scene, t, th);
49
+ if (t >= QR_DOOR_DURATION) {
50
+ if (!finished) {
51
+ finished = true;
52
+ onDoneRef.current?.();
53
+ }
54
+ }
55
+ else {
56
+ raf = requestAnimationFrame(paint);
57
+ }
58
+ };
59
+ paint();
60
+ const resize = new ResizeObserver(() => {
61
+ if (!approved || finished)
62
+ paint();
63
+ });
64
+ resize.observe(canvas);
65
+ return () => {
66
+ cancelAnimationFrame(raf);
67
+ resize.disconnect();
68
+ };
69
+ }, [scene, approved, ink, accent, paper]);
70
+ return (_jsx("canvas", { ref: canvasRef, className: className, style: { display: 'block', width: '100%', aspectRatio: '1' }, role: "img", "aria-label": approved ? 'Sign-in approved' : 'Scan this code with your phone to sign in' }));
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/auth",
3
- "version": "0.19.0",
3
+ "version": "0.22.0",
4
4
  "description": "Sign in against a self-hosted ZITADEL issuer: a server session for a Next.js app, and a browser client with silent single sign-on across subdomains.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,6 +42,14 @@
42
42
  "types": "./dist/sessions.d.ts",
43
43
  "default": "./dist/sessions.js"
44
44
  },
45
+ "./person-token": {
46
+ "types": "./dist/person-token.d.ts",
47
+ "default": "./dist/person-token.js"
48
+ },
49
+ "./qr-door": {
50
+ "types": "./dist/qr-door.d.ts",
51
+ "default": "./dist/qr-door.js"
52
+ },
45
53
  "./testing": {
46
54
  "types": "./dist/testing.d.ts",
47
55
  "default": "./dist/testing.js"
@@ -59,25 +67,32 @@
59
67
  "test:cookie-browser": "vitest run --config vitest.cookie-browser.config.ts"
60
68
  },
61
69
  "dependencies": {
70
+ "@wtfalch/utils": "0.1.0",
62
71
  "jose": "^6.2.10",
63
72
  "obscenity": "^0.4.6",
64
73
  "openid-client": "6.8.7",
74
+ "qrcode": "^1.5.4",
65
75
  "reserved-usernames": "^1.1.6"
66
76
  },
67
77
  "peerDependencies": {
68
- "next": ">=15.0.0"
78
+ "next": ">=15.0.0",
79
+ "react": ">=19.0.0"
69
80
  },
70
81
  "peerDependenciesMeta": {
71
82
  "next": {
72
83
  "optional": true
84
+ },
85
+ "react": {
86
+ "optional": true
73
87
  }
74
88
  },
75
89
  "devDependencies": {
76
90
  "@electric-sql/pglite": "^0.5.8",
77
91
  "@types/node": "^22",
92
+ "@types/qrcode": "^1.5.6",
78
93
  "@types/react": "^19",
79
94
  "@wtfalch/auth-broker": "workspace:*",
80
- "next": "^16",
95
+ "next": "^16.4.0",
81
96
  "playwright-core": "^1.62.1",
82
97
  "react": "^19",
83
98
  "typescript": "^5.9.0",