@navette-js/panels 1.0.0-beta.3 → 1.0.0-beta.5
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.
- package/THIRD-PARTY-NOTICES.md +2 -1
- package/dist/navette/navette/calendar.css +5 -5
- package/dist/navette/navette/motion.css +21 -21
- package/dist/navette/navette/motion.js +6 -6
- package/dist/navette/navette/security-keys.css +11 -0
- package/dist/navette/navette/security-keys.js +72 -0
- package/lib/auth.d.ts +17 -0
- package/lib/auth.js +70 -0
- package/lib/error-pages.d.ts +9 -0
- package/lib/error-pages.js +173 -0
- package/lib/import-export.d.ts +185 -0
- package/lib/import-export.js +543 -0
- package/lib/index.d.ts +629 -79
- package/lib/index.js +1945 -333
- package/lib/multi-factor.d.ts +348 -0
- package/lib/multi-factor.js +928 -0
- package/lib/notifications.d.ts +20 -2
- package/lib/notifications.js +34 -4
- package/lib/qr-code.d.ts +16 -0
- package/lib/qr-code.js +255 -0
- package/lib/session-store.d.ts +21 -0
- package/lib/session-store.js +35 -0
- package/lib/throttle.d.ts +10 -0
- package/lib/throttle.js +25 -0
- package/lib/topbar.d.ts +3 -1
- package/lib/topbar.js +18 -1
- package/lib/totp.d.ts +38 -0
- package/lib/totp.js +104 -0
- package/lib/webauthn.d.ts +155 -0
- package/lib/webauthn.js +287 -0
- package/lib/xlsx.d.ts +7 -0
- package/lib/xlsx.js +171 -0
- package/package.json +9 -9
- package/views/components/tenant-menu.ejs +3 -3
- package/views/components/user-menu.ejs +3 -2
- package/views/errors/page.ejs +87 -0
- package/views/layout/base.ejs +9 -1
- package/views/livewire/sidebar.ejs +35 -20
- package/views/livewire/topbar.ejs +1 -1
- package/views/pages/page.ejs +53 -35
- package/views/pages/simple.ejs +4 -0
package/lib/notifications.d.ts
CHANGED
|
@@ -3,6 +3,13 @@ export type NotificationActionData = {
|
|
|
3
3
|
label: string;
|
|
4
4
|
url: string | null;
|
|
5
5
|
openUrlInNewTab: boolean;
|
|
6
|
+
close?: boolean;
|
|
7
|
+
dispatch?: {
|
|
8
|
+
event: string;
|
|
9
|
+
data: Record<string, unknown>;
|
|
10
|
+
} | null;
|
|
11
|
+
color?: string | null;
|
|
12
|
+
button?: boolean;
|
|
6
13
|
};
|
|
7
14
|
export type NotificationData = {
|
|
8
15
|
id: string;
|
|
@@ -40,11 +47,22 @@ export declare class Notification {
|
|
|
40
47
|
iconColor(color: string): this;
|
|
41
48
|
duration(ms: number): this;
|
|
42
49
|
persistent(): this;
|
|
43
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Actions under the text: links (`url`), or buttons that `close` the notification and/or `dispatch` a Livewire event
|
|
52
|
+
* (`{ event: 'undoDelete', data: { id: 5 } }`). `button: true` renders a button instead of a link. Buttons are only shown in
|
|
53
|
+
* flash notifications; the database notifications panel shows the links.
|
|
54
|
+
*/
|
|
44
55
|
actions(actions: {
|
|
45
56
|
label: string;
|
|
46
57
|
url?: string | null;
|
|
47
58
|
openUrlInNewTab?: boolean;
|
|
59
|
+
close?: boolean;
|
|
60
|
+
dispatch?: {
|
|
61
|
+
event: string;
|
|
62
|
+
data?: Record<string, unknown>;
|
|
63
|
+
} | null;
|
|
64
|
+
color?: string | null;
|
|
65
|
+
button?: boolean;
|
|
48
66
|
}[]): this;
|
|
49
67
|
getData(): NotificationData;
|
|
50
68
|
/** Stores the notification for this user (panel bell): `await Notification.make()….sendToDatabase(panel.getDatabaseNotifications(), user.id)`. Does nothing without a store. */
|
|
@@ -52,7 +70,7 @@ export declare class Notification {
|
|
|
52
70
|
/** Queues the notification for the browser of the current request. */
|
|
53
71
|
send(): this;
|
|
54
72
|
}
|
|
55
|
-
/** Renders a flash notification (no date
|
|
73
|
+
/** Renders a flash notification (no date or inline mode). Title, body and action labels are escaped. */
|
|
56
74
|
export declare function renderNotification(n: NotificationData): string;
|
|
57
75
|
/** Container component for flash notifications. */
|
|
58
76
|
export declare class Notifications extends Component {
|
package/lib/notifications.js
CHANGED
|
@@ -53,8 +53,24 @@ export class Notification {
|
|
|
53
53
|
iconColor(color) { this.data.iconColor = color; return this; }
|
|
54
54
|
duration(ms) { this.data.duration = ms; return this; }
|
|
55
55
|
persistent() { this.data.duration = 'persistent'; return this; }
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Actions under the text: links (`url`), or buttons that `close` the notification and/or `dispatch` a Livewire event
|
|
58
|
+
* (`{ event: 'undoDelete', data: { id: 5 } }`). `button: true` renders a button instead of a link. Buttons are only shown in
|
|
59
|
+
* flash notifications; the database notifications panel shows the links.
|
|
60
|
+
*/
|
|
61
|
+
actions(actions) {
|
|
62
|
+
this.data.actions = actions.slice(0, 10).map((action) => {
|
|
63
|
+
const event = action.dispatch?.event;
|
|
64
|
+
if (event !== undefined && !/^[A-Za-z0-9_.:-]{1,100}$/.test(event))
|
|
65
|
+
throw new Error(`Invalid notification event name: ${event}`);
|
|
66
|
+
return {
|
|
67
|
+
label: String(action.label), url: action.url ?? null, openUrlInNewTab: action.openUrlInNewTab === true, close: action.close === true,
|
|
68
|
+
dispatch: event ? { event, data: action.dispatch?.data ?? {} } : null,
|
|
69
|
+
color: action.color && /^[a-z]+$/.test(action.color) ? action.color : null, button: action.button === true,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
58
74
|
getData() { return { ...this.data }; }
|
|
59
75
|
/** Stores the notification for this user (panel bell): `await Notification.make()….sendToDatabase(panel.getDatabaseNotifications(), user.id)`. Does nothing without a store. */
|
|
60
76
|
async sendToDatabase(store, user) { if (store)
|
|
@@ -68,7 +84,21 @@ export class Notification {
|
|
|
68
84
|
return this;
|
|
69
85
|
}
|
|
70
86
|
}
|
|
71
|
-
/**
|
|
87
|
+
/** Actions of a notification: links (only local or http(s) URLs) and buttons closing it or dispatching a Livewire event. */
|
|
88
|
+
function notificationActionsHtml(n) {
|
|
89
|
+
const safeUrl = (url) => typeof url === 'string' && /^(\/(?!\/)|https?:\/\/)/.test(url);
|
|
90
|
+
const items = (n.actions ?? []).filter((action) => action.label !== '' && (safeUrl(action.url) || action.close || action.dispatch)).map((action) => {
|
|
91
|
+
const color = action.color ?? 'primary';
|
|
92
|
+
const classes = action.button ? `fi-ac-btn-action fi-btn fi-size-sm` : `fi-ac-link-action fi-link fi-size-sm fi-color fi-color-${color} fi-text-color-600 dark:fi-text-color-400`;
|
|
93
|
+
const label = action.button ? e(action.label) : `<span class="fi-link-label">${e(action.label)}</span>`;
|
|
94
|
+
const clicks = [action.dispatch ? `Livewire.dispatch(${js(action.dispatch.event)}, ${js(action.dispatch.data)})` : null, action.close ? 'close()' : null].filter(Boolean).join('; ');
|
|
95
|
+
if (safeUrl(action.url))
|
|
96
|
+
return `<a href="${e(action.url)}"${action.openUrlInNewTab ? ' target="_blank" rel="noopener noreferrer"' : ''}${clicks ? ` x-on:click="${e(clicks)}"` : ''} class="${classes}">${label}</a>`;
|
|
97
|
+
return `<button type="button" x-on:click="${e(clicks)}" class="${classes}">${label}</button>`;
|
|
98
|
+
});
|
|
99
|
+
return items.length ? `<div class="fi-ac fi-no-notification-actions">${items.join('')}</div>` : '';
|
|
100
|
+
}
|
|
101
|
+
/** Renders a flash notification (no date or inline mode). Title, body and action labels are escaped. */
|
|
72
102
|
export function renderNotification(n) {
|
|
73
103
|
const closeLabel = t('filament-notifications::notification.actions.close.label');
|
|
74
104
|
const attributes = new Attributes()
|
|
@@ -77,7 +107,7 @@ export function renderNotification(n) {
|
|
|
77
107
|
const iconName = n.icon ?? (n.status ? STATUS_ICON[n.status] : null);
|
|
78
108
|
const iconColor = n.iconColor ?? n.status;
|
|
79
109
|
const iconHtml = iconName ? icon(iconName, { size: 'lg', attributes: new Attributes().class([...(iconColor && iconColor !== 'gray' ? ['fi-color', `fi-color-${iconColor}`] : []), 'fi-no-notification-icon']) }) : '';
|
|
80
|
-
return `<div x-data="notificationComponent({ notification: ${e(js({ id: n.id, duration: n.duration }))} })" x-transition:enter-start="fi-transition-enter-start" x-transition:enter-end="fi-transition-enter-end" x-transition:leave-start="fi-transition-leave-start" x-transition:leave-end="fi-transition-leave-end" x-on:mouseenter="pauseDuration('hover')" x-on:mouseleave="resumeDuration('hover')" x-on:focusin="handleFocusIn($event)" x-on:focusout="handleFocusOut($event)" x-on:keydown.escape="handleEscape($event)" ${attributes.toHtml().replace(/"if \(\$event\.detail\.id == '([^']*)'\) close\(\)"/, (_m, id) => `"if ($event.detail.id == '${id}') close()"`)}>${iconHtml}<div class="fi-no-notification-main">${n.title !== null || n.body !== null ? `<div class="fi-no-notification-text">${n.title !== null ? `<h3 class="fi-no-notification-title">${e(n.title)}</h3>` : ''}${n.body !== null ? `<div class="fi-no-notification-body">${e(n.body)}</div>` : ''}</div>` : ''}</div><button type="button" x-on:click="dismiss()" aria-label="${e(closeLabel)}" title="${e(closeLabel)}" class="fi-icon-btn fi-no-notification-close-btn">${icon('m-x-mark')}</button></div>`;
|
|
110
|
+
return `<div x-data="notificationComponent({ notification: ${e(js({ id: n.id, duration: n.duration }))} })" x-transition:enter-start="fi-transition-enter-start" x-transition:enter-end="fi-transition-enter-end" x-transition:leave-start="fi-transition-leave-start" x-transition:leave-end="fi-transition-leave-end" x-on:mouseenter="pauseDuration('hover')" x-on:mouseleave="resumeDuration('hover')" x-on:focusin="handleFocusIn($event)" x-on:focusout="handleFocusOut($event)" x-on:keydown.escape="handleEscape($event)" ${attributes.toHtml().replace(/"if \(\$event\.detail\.id == '([^']*)'\) close\(\)"/, (_m, id) => `"if ($event.detail.id == '${id}') close()"`)}>${iconHtml}<div class="fi-no-notification-main">${n.title !== null || n.body !== null ? `<div class="fi-no-notification-text">${n.title !== null ? `<h3 class="fi-no-notification-title">${e(n.title)}</h3>` : ''}${n.body !== null ? `<div class="fi-no-notification-body">${e(n.body)}</div>` : ''}</div>` : ''}${notificationActionsHtml(n)}</div><button type="button" x-on:click="dismiss()" aria-label="${e(closeLabel)}" title="${e(closeLabel)}" class="fi-icon-btn fi-no-notification-close-btn">${icon('m-x-mark')}</button></div>`;
|
|
81
111
|
}
|
|
82
112
|
/** Container component for flash notifications. */
|
|
83
113
|
export class Notifications extends Component {
|
package/lib/qr-code.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type QrErrorCorrection = 'L' | 'M' | 'Q' | 'H';
|
|
2
|
+
/** Module matrix of a QR Code: `true` for a dark module. Raises an error when the data does not fit (about 2.3 KB at level M). */
|
|
3
|
+
export declare function qrCodeMatrix(text: string | Uint8Array, errorCorrection?: QrErrorCorrection): boolean[][];
|
|
4
|
+
export type QrCodeSvgOptions = {
|
|
5
|
+
errorCorrection?: QrErrorCorrection;
|
|
6
|
+
/** Quiet zone around the code, in modules (4 by default, as the standard requires). */
|
|
7
|
+
margin?: number;
|
|
8
|
+
dark?: string;
|
|
9
|
+
light?: string;
|
|
10
|
+
/** Accessible name of the image (`<title>`). */
|
|
11
|
+
title?: string | null;
|
|
12
|
+
};
|
|
13
|
+
/** QR Code as an SVG document (scalable: give it a size with CSS). */
|
|
14
|
+
export declare function qrCodeSvg(text: string, options?: QrCodeSvgOptions): string;
|
|
15
|
+
/** QR Code as a `data:` URI, for an `<img src>`. */
|
|
16
|
+
export declare const qrCodeDataUri: (text: string, options?: QrCodeSvgOptions) => string;
|
package/lib/qr-code.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// QR Code generator (ISO/IEC 18004), byte mode, versions 1 to 40, rendered as SVG. No dependency.
|
|
2
|
+
// Follows the structure of Project Nayuki's QR Code generator (https://www.nayuki.io/page/qr-code-generator-library):
|
|
3
|
+
// Copyright (c) Project Nayuki. MIT License: permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
// this software and associated documentation files, to deal in the Software without restriction, subject to including this
|
|
5
|
+
// notice; the Software is provided "as is", without warranty of any kind.
|
|
6
|
+
const LEVELS = { L: { ordinal: 0, formatBits: 1 }, M: { ordinal: 1, formatBits: 0 }, Q: { ordinal: 2, formatBits: 3 }, H: { ordinal: 3, formatBits: 2 } };
|
|
7
|
+
const ECC_CODEWORDS_PER_BLOCK = [
|
|
8
|
+
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
9
|
+
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
10
|
+
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
11
|
+
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
12
|
+
];
|
|
13
|
+
const NUM_ERROR_CORRECTION_BLOCKS = [
|
|
14
|
+
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
|
|
15
|
+
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
16
|
+
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
17
|
+
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81],
|
|
18
|
+
];
|
|
19
|
+
const bit = (value, index) => ((value >>> index) & 1) !== 0;
|
|
20
|
+
function rawDataModules(version) {
|
|
21
|
+
let result = (16 * version + 128) * version + 64;
|
|
22
|
+
if (version >= 2) {
|
|
23
|
+
const alignments = Math.floor(version / 7) + 2;
|
|
24
|
+
result -= (25 * alignments - 10) * alignments - 55;
|
|
25
|
+
if (version >= 7)
|
|
26
|
+
result -= 36;
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
const dataCodewords = (version, level) => Math.floor(rawDataModules(version) / 8) - ECC_CODEWORDS_PER_BLOCK[level][version] * NUM_ERROR_CORRECTION_BLOCKS[level][version];
|
|
31
|
+
// Reed-Solomon over GF(2^8), polynomial 0x11D.
|
|
32
|
+
function multiply(x, y) {
|
|
33
|
+
let z = 0;
|
|
34
|
+
for (let i = 7; i >= 0; i--) {
|
|
35
|
+
z = (z << 1) ^ ((z >>> 7) * 0x11d);
|
|
36
|
+
z ^= ((y >>> i) & 1) * x;
|
|
37
|
+
}
|
|
38
|
+
return z & 0xff;
|
|
39
|
+
}
|
|
40
|
+
function divisor(degree) {
|
|
41
|
+
const result = new Array(degree - 1).fill(0).concat([1]);
|
|
42
|
+
let root = 1;
|
|
43
|
+
for (let i = 0; i < degree; i++) {
|
|
44
|
+
for (let j = 0; j < result.length; j++) {
|
|
45
|
+
result[j] = multiply(result[j], root);
|
|
46
|
+
if (j + 1 < result.length)
|
|
47
|
+
result[j] ^= result[j + 1];
|
|
48
|
+
}
|
|
49
|
+
root = multiply(root, 0x02);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
function remainder(data, generator) {
|
|
54
|
+
const result = generator.map(() => 0);
|
|
55
|
+
for (const byte of data) {
|
|
56
|
+
const factor = byte ^ result.shift();
|
|
57
|
+
result.push(0);
|
|
58
|
+
generator.forEach((coefficient, i) => { result[i] ^= multiply(coefficient, factor); });
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/** Module matrix of a QR Code: `true` for a dark module. Raises an error when the data does not fit (about 2.3 KB at level M). */
|
|
63
|
+
export function qrCodeMatrix(text, errorCorrection = 'M') {
|
|
64
|
+
const level = LEVELS[errorCorrection];
|
|
65
|
+
if (!level)
|
|
66
|
+
throw new Error(`Unknown error correction level: ${errorCorrection}`);
|
|
67
|
+
const bytes = typeof text === 'string' ? new TextEncoder().encode(text) : text;
|
|
68
|
+
let version = 1;
|
|
69
|
+
for (;; version++) {
|
|
70
|
+
if (version > 40)
|
|
71
|
+
throw new Error('Data too long for a QR Code.');
|
|
72
|
+
const countBits = version <= 9 ? 8 : 16;
|
|
73
|
+
if (4 + countBits + bytes.length * 8 <= dataCodewords(version, level.ordinal) * 8)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
// Data bits: byte mode, character count, bytes, terminator, padding.
|
|
77
|
+
const bits = [];
|
|
78
|
+
const append = (value, length) => { for (let i = length - 1; i >= 0; i--)
|
|
79
|
+
bits.push((value >>> i) & 1); };
|
|
80
|
+
append(0b0100, 4);
|
|
81
|
+
append(bytes.length, version <= 9 ? 8 : 16);
|
|
82
|
+
for (const byte of bytes)
|
|
83
|
+
append(byte, 8);
|
|
84
|
+
const capacity = dataCodewords(version, level.ordinal) * 8;
|
|
85
|
+
append(0, Math.min(4, capacity - bits.length));
|
|
86
|
+
append(0, (8 - (bits.length % 8)) % 8);
|
|
87
|
+
for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11)
|
|
88
|
+
append(pad, 8);
|
|
89
|
+
const data = [];
|
|
90
|
+
for (let i = 0; i < bits.length; i += 8)
|
|
91
|
+
data.push(bits.slice(i, i + 8).reduce((byte, b) => (byte << 1) | b, 0));
|
|
92
|
+
// Error correction blocks, interleaved.
|
|
93
|
+
const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[level.ordinal][version];
|
|
94
|
+
const blockEccLength = ECC_CODEWORDS_PER_BLOCK[level.ordinal][version];
|
|
95
|
+
const rawCodewords = Math.floor(rawDataModules(version) / 8);
|
|
96
|
+
const numShortBlocks = numBlocks - (rawCodewords % numBlocks);
|
|
97
|
+
const shortBlockLength = Math.floor(rawCodewords / numBlocks);
|
|
98
|
+
const generator = divisor(blockEccLength);
|
|
99
|
+
const blocks = [];
|
|
100
|
+
for (let i = 0, k = 0; i < numBlocks; i++) {
|
|
101
|
+
const block = data.slice(k, k + shortBlockLength - blockEccLength + (i < numShortBlocks ? 0 : 1));
|
|
102
|
+
k += block.length;
|
|
103
|
+
const ecc = remainder(block, generator);
|
|
104
|
+
if (i < numShortBlocks)
|
|
105
|
+
block.push(0);
|
|
106
|
+
blocks.push(block.concat(ecc));
|
|
107
|
+
}
|
|
108
|
+
const codewords = [];
|
|
109
|
+
for (let i = 0; i < blocks[0].length; i++)
|
|
110
|
+
blocks.forEach((block, j) => { if (i !== shortBlockLength - blockEccLength || j >= numShortBlocks)
|
|
111
|
+
codewords.push(block[i]); });
|
|
112
|
+
// Function patterns.
|
|
113
|
+
const size = version * 4 + 17;
|
|
114
|
+
const modules = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
115
|
+
const isFunction = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
116
|
+
const set = (x, y, dark) => { modules[y][x] = dark; isFunction[y][x] = true; };
|
|
117
|
+
for (let i = 0; i < size; i++) {
|
|
118
|
+
set(6, i, i % 2 === 0);
|
|
119
|
+
set(i, 6, i % 2 === 0);
|
|
120
|
+
}
|
|
121
|
+
for (const [cx, cy] of [[3, 3], [size - 4, 3], [3, size - 4]]) {
|
|
122
|
+
for (let dy = -4; dy <= 4; dy++)
|
|
123
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
124
|
+
const distance = Math.max(Math.abs(dx), Math.abs(dy)), x = cx + dx, y = cy + dy;
|
|
125
|
+
if (x >= 0 && x < size && y >= 0 && y < size)
|
|
126
|
+
set(x, y, distance !== 2 && distance !== 4);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const alignments = [];
|
|
130
|
+
if (version > 1) {
|
|
131
|
+
const count = Math.floor(version / 7) + 2;
|
|
132
|
+
const step = Math.floor((version * 8 + count * 3 + 5) / (count * 4 - 4)) * 2;
|
|
133
|
+
alignments.push(6);
|
|
134
|
+
for (let position = size - 7; alignments.length < count; position -= step)
|
|
135
|
+
alignments.splice(1, 0, position);
|
|
136
|
+
}
|
|
137
|
+
alignments.forEach((cx, i) => alignments.forEach((cy, j) => {
|
|
138
|
+
if ((i === 0 && j === 0) || (i === 0 && j === alignments.length - 1) || (i === alignments.length - 1 && j === 0))
|
|
139
|
+
return;
|
|
140
|
+
for (let dy = -2; dy <= 2; dy++)
|
|
141
|
+
for (let dx = -2; dx <= 2; dx++)
|
|
142
|
+
set(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
143
|
+
}));
|
|
144
|
+
const drawFormat = (mask) => {
|
|
145
|
+
const formatData = (level.formatBits << 3) | mask;
|
|
146
|
+
let rem = formatData;
|
|
147
|
+
for (let i = 0; i < 10; i++)
|
|
148
|
+
rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
149
|
+
const format = ((formatData << 10) | rem) ^ 0x5412;
|
|
150
|
+
for (let i = 0; i <= 5; i++)
|
|
151
|
+
set(8, i, bit(format, i));
|
|
152
|
+
set(8, 7, bit(format, 6));
|
|
153
|
+
set(8, 8, bit(format, 7));
|
|
154
|
+
set(7, 8, bit(format, 8));
|
|
155
|
+
for (let i = 9; i < 15; i++)
|
|
156
|
+
set(14 - i, 8, bit(format, i));
|
|
157
|
+
for (let i = 0; i < 8; i++)
|
|
158
|
+
set(size - 1 - i, 8, bit(format, i));
|
|
159
|
+
for (let i = 8; i < 15; i++)
|
|
160
|
+
set(8, size - 15 + i, bit(format, i));
|
|
161
|
+
set(8, size - 8, true);
|
|
162
|
+
};
|
|
163
|
+
drawFormat(0); // reserves the format areas before the data is placed
|
|
164
|
+
if (version >= 7) {
|
|
165
|
+
let rem = version;
|
|
166
|
+
for (let i = 0; i < 12; i++)
|
|
167
|
+
rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
|
|
168
|
+
const versionBits = (version << 12) | rem;
|
|
169
|
+
for (let i = 0; i < 18; i++) {
|
|
170
|
+
const a = size - 11 + (i % 3), b = Math.floor(i / 3);
|
|
171
|
+
set(a, b, bit(versionBits, i));
|
|
172
|
+
set(b, a, bit(versionBits, i));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Codewords, in the zigzag order.
|
|
176
|
+
let index = 0;
|
|
177
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
178
|
+
if (right === 6)
|
|
179
|
+
right = 5;
|
|
180
|
+
for (let vertical = 0; vertical < size; vertical++)
|
|
181
|
+
for (let j = 0; j < 2; j++) {
|
|
182
|
+
const x = right - j, upward = ((right + 1) & 2) === 0, y = upward ? size - 1 - vertical : vertical;
|
|
183
|
+
if (!isFunction[y][x] && index < codewords.length * 8) {
|
|
184
|
+
modules[y][x] = bit(codewords[index >>> 3], 7 - (index & 7));
|
|
185
|
+
index++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Mask: the one with the lowest penalty.
|
|
190
|
+
const masks = [
|
|
191
|
+
(x, y) => (x + y) % 2 === 0, (_x, y) => y % 2 === 0, (x) => x % 3 === 0, (x, y) => (x + y) % 3 === 0,
|
|
192
|
+
(x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0, (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
193
|
+
(x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0, (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
194
|
+
];
|
|
195
|
+
const applyMask = (mask) => { for (let y = 0; y < size; y++)
|
|
196
|
+
for (let x = 0; x < size; x++)
|
|
197
|
+
if (!isFunction[y][x] && masks[mask](x, y))
|
|
198
|
+
modules[y][x] = !modules[y][x]; };
|
|
199
|
+
let best = 0, bestPenalty = Infinity;
|
|
200
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
201
|
+
applyMask(mask);
|
|
202
|
+
drawFormat(mask);
|
|
203
|
+
const score = penalty(modules);
|
|
204
|
+
if (score < bestPenalty) {
|
|
205
|
+
best = mask;
|
|
206
|
+
bestPenalty = score;
|
|
207
|
+
}
|
|
208
|
+
applyMask(mask); // XOR again: undoes the mask
|
|
209
|
+
}
|
|
210
|
+
applyMask(best);
|
|
211
|
+
drawFormat(best);
|
|
212
|
+
return modules;
|
|
213
|
+
}
|
|
214
|
+
function penalty(modules) {
|
|
215
|
+
const size = modules.length;
|
|
216
|
+
let score = 0;
|
|
217
|
+
const lines = [...modules, ...modules[0].map((_, x) => modules.map((row) => row[x]))];
|
|
218
|
+
const finderLike = [[true, false, true, true, true, false, true, false, false, false, false], [false, false, false, false, true, false, true, true, true, false, true]];
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
for (let i = 0, run = 1; i < size; i++, run++) {
|
|
221
|
+
if (i === size - 1 || line[i] !== line[i + 1]) {
|
|
222
|
+
if (run >= 5)
|
|
223
|
+
score += 3 + run - 5;
|
|
224
|
+
run = 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (let i = 0; i + 11 <= size; i++)
|
|
228
|
+
for (const pattern of finderLike)
|
|
229
|
+
if (pattern.every((dark, k) => line[i + k] === dark))
|
|
230
|
+
score += 40;
|
|
231
|
+
}
|
|
232
|
+
for (let y = 0; y + 1 < size; y++)
|
|
233
|
+
for (let x = 0; x + 1 < size; x++) {
|
|
234
|
+
const color = modules[y][x];
|
|
235
|
+
if (color === modules[y][x + 1] && color === modules[y + 1][x] && color === modules[y + 1][x + 1])
|
|
236
|
+
score += 3;
|
|
237
|
+
}
|
|
238
|
+
const dark = modules.reduce((sum, row) => sum + row.filter(Boolean).length, 0), total = size * size;
|
|
239
|
+
score += (Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1) * 10;
|
|
240
|
+
return score;
|
|
241
|
+
}
|
|
242
|
+
const escapeXml = (value) => value.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
243
|
+
/** QR Code as an SVG document (scalable: give it a size with CSS). */
|
|
244
|
+
export function qrCodeSvg(text, options = {}) {
|
|
245
|
+
const modules = qrCodeMatrix(text, options.errorCorrection ?? 'M');
|
|
246
|
+
const margin = Math.max(0, Math.floor(options.margin ?? 4));
|
|
247
|
+
const size = modules.length + margin * 2;
|
|
248
|
+
let path = '';
|
|
249
|
+
modules.forEach((row, y) => row.forEach((dark, x) => { if (dark)
|
|
250
|
+
path += `M${x + margin},${y + margin}h1v1h-1z`; }));
|
|
251
|
+
const color = (value, fallback) => escapeXml(value ?? fallback);
|
|
252
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges">${options.title ? `<title>${escapeXml(options.title)}</title>` : ''}<rect width="100%" height="100%" fill="${color(options.light, '#ffffff')}"/><path d="${path}" fill="${color(options.dark, '#000000')}"/></svg>`;
|
|
253
|
+
}
|
|
254
|
+
/** QR Code as a `data:` URI, for an `<img src>`. */
|
|
255
|
+
export const qrCodeDataUri = (text, options = {}) => `data:image/svg+xml;base64,${Buffer.from(qrCodeSvg(text, options)).toString('base64')}`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Storage of small per-session values. Implement it to keep them in a database or a cache shared by several processes. */
|
|
2
|
+
export interface SessionStore {
|
|
3
|
+
get(session: string, key: string): unknown | Promise<unknown>;
|
|
4
|
+
set(session: string, key: string, value: unknown): void | Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* In-memory store: values are lost when the process restarts and are not shared between processes.
|
|
8
|
+
* Sessions idle for longer than `ttl` are dropped, and the oldest sessions go first beyond `maxSessions`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class MemorySessionStore implements SessionStore {
|
|
11
|
+
private sessions;
|
|
12
|
+
private readonly options;
|
|
13
|
+
constructor(options?: {
|
|
14
|
+
ttl?: number;
|
|
15
|
+
maxSessions?: number;
|
|
16
|
+
maxValueLength?: number;
|
|
17
|
+
});
|
|
18
|
+
private get ttl();
|
|
19
|
+
get(session: string, key: string): unknown;
|
|
20
|
+
set(session: string, key: string, value: unknown): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Per-browser session data (persisted table filters, search and sort). The default store keeps it in memory.
|
|
2
|
+
/**
|
|
3
|
+
* In-memory store: values are lost when the process restarts and are not shared between processes.
|
|
4
|
+
* Sessions idle for longer than `ttl` are dropped, and the oldest sessions go first beyond `maxSessions`.
|
|
5
|
+
*/
|
|
6
|
+
export class MemorySessionStore {
|
|
7
|
+
sessions = new Map();
|
|
8
|
+
options;
|
|
9
|
+
constructor(options = {}) { this.options = options; }
|
|
10
|
+
get ttl() { return this.options.ttl ?? 12 * 60 * 60 * 1000; }
|
|
11
|
+
get(session, key) {
|
|
12
|
+
const entry = this.sessions.get(session);
|
|
13
|
+
if (!entry || Date.now() - entry.at > this.ttl) {
|
|
14
|
+
this.sessions.delete(session);
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const raw = entry.values.get(key);
|
|
18
|
+
return raw === undefined ? undefined : JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
set(session, key, value) {
|
|
21
|
+
const raw = JSON.stringify(value ?? null);
|
|
22
|
+
// Values come from the browser: an oversized one is not kept.
|
|
23
|
+
if (raw.length > (this.options.maxValueLength ?? 64 * 1024))
|
|
24
|
+
return;
|
|
25
|
+
const entry = this.sessions.get(session) ?? { at: Date.now(), values: new Map() };
|
|
26
|
+
entry.at = Date.now();
|
|
27
|
+
entry.values.set(key, raw);
|
|
28
|
+
// Most recently used last: re-inserting moves the session to the end.
|
|
29
|
+
this.sessions.delete(session);
|
|
30
|
+
this.sessions.set(session, entry);
|
|
31
|
+
const max = this.options.maxSessions ?? 10000;
|
|
32
|
+
while (this.sessions.size > max)
|
|
33
|
+
this.sessions.delete(this.sessions.keys().next().value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** In-memory rate limiter: `max` attempts per window, per key. Fine for a single instance; use a shared store when running several. */
|
|
2
|
+
export declare class Throttle {
|
|
3
|
+
private hits;
|
|
4
|
+
private max;
|
|
5
|
+
private windowMs;
|
|
6
|
+
constructor(max?: number, windowMs?: number);
|
|
7
|
+
/** Records an attempt; returns 0 if allowed, otherwise the number of seconds to wait. */
|
|
8
|
+
hit(key: string, now?: number): number;
|
|
9
|
+
clear(key: string): void;
|
|
10
|
+
}
|
package/lib/throttle.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Rate limiting of sensitive endpoints (login, password reset, multi-factor challenges).
|
|
2
|
+
/** In-memory rate limiter: `max` attempts per window, per key. Fine for a single instance; use a shared store when running several. */
|
|
3
|
+
export class Throttle {
|
|
4
|
+
hits = new Map();
|
|
5
|
+
max;
|
|
6
|
+
windowMs;
|
|
7
|
+
constructor(max = 5, windowMs = 60_000) { this.max = max; this.windowMs = windowMs; }
|
|
8
|
+
/** Records an attempt; returns 0 if allowed, otherwise the number of seconds to wait. */
|
|
9
|
+
hit(key, now = Date.now()) {
|
|
10
|
+
const recent = (this.hits.get(key) ?? []).filter((at) => at > now - this.windowMs);
|
|
11
|
+
if (recent.length >= this.max) {
|
|
12
|
+
this.hits.set(key, recent);
|
|
13
|
+
return Math.max(1, Math.ceil((recent[0] + this.windowMs - now) / 1000));
|
|
14
|
+
}
|
|
15
|
+
recent.push(now);
|
|
16
|
+
this.hits.set(key, recent);
|
|
17
|
+
// Keep the store from growing forever.
|
|
18
|
+
if (this.hits.size > 10_000)
|
|
19
|
+
for (const [k, v] of this.hits)
|
|
20
|
+
if (!v.some((at) => at > now - this.windowMs))
|
|
21
|
+
this.hits.delete(k);
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
clear(key) { this.hits.delete(key); }
|
|
25
|
+
}
|
package/lib/topbar.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export declare const notificationBodyHtml: (body: string) => string;
|
|
|
5
5
|
export type GlobalSearchResult = {
|
|
6
6
|
title: string;
|
|
7
7
|
url: string;
|
|
8
|
-
details: Record<string, string>;
|
|
8
|
+
details: Record<string, string>; /** Buttons under the result: links (`url()`) or Livewire events (`dispatch()`). */
|
|
9
|
+
actions?: any[];
|
|
10
|
+
record?: unknown;
|
|
9
11
|
};
|
|
10
12
|
export type GlobalSearchCategory = {
|
|
11
13
|
label: string;
|
package/lib/topbar.js
CHANGED
|
@@ -54,6 +54,22 @@ export class GlobalSearch extends Component {
|
|
|
54
54
|
}).class(['fi-input', 'fi-input-has-inline-prefix']).toHtml()} />`;
|
|
55
55
|
const focus = "if (! $event.target.closest('.fi-input-wrp-actions')) $dispatch('focus-input')";
|
|
56
56
|
const field = `<div class="fi-input-wrp"><div x-on:click="${e(focus)}" class="fi-input-wrp-prefix fi-input-wrp-prefix-has-content fi-inline">${icon('m-magnifying-glass', { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': 'search' }) })}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': 'search' }))}</div><div class="fi-input-wrp-content-ctn">${input}</div>${suffix ? `<div x-on:click="${e(focus)}" class="fi-input-wrp-suffix fi-inline fi-input-wrp-suffix-has-label"><span class="fi-input-wrp-label">${e(suffix)}</span></div>` : ''}</div>`;
|
|
57
|
+
// Actions of each result: rendered first (async), then placed under the result's link.
|
|
58
|
+
const actionsHtml = new Map();
|
|
59
|
+
for (const [groupIndex, category] of (categories ?? []).entries()) {
|
|
60
|
+
for (const [index, result] of category.results.entries()) {
|
|
61
|
+
const rendered = [];
|
|
62
|
+
for (const action of result.actions ?? []) {
|
|
63
|
+
const ctx = { livewire: this, component: null, operation: null, record: result.record ?? null, get: () => undefined, state: undefined };
|
|
64
|
+
if (!(await action.isVisible(ctx)))
|
|
65
|
+
continue;
|
|
66
|
+
const describedBy = `${id}-group-${groupIndex} ${id}-group-${groupIndex}-result-${index}`;
|
|
67
|
+
rendered.push(String(await action.toHtml(ctx)).replace(/^<(a|button)\s/, (_m, tag) => `<${tag} aria-describedby="${e(describedBy)}" data-global-search-action="1" `));
|
|
68
|
+
}
|
|
69
|
+
if (rendered.length)
|
|
70
|
+
actionsHtml.set(result, rendered);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
57
73
|
let results = '';
|
|
58
74
|
if (categories !== null) {
|
|
59
75
|
results = categories.length === 0
|
|
@@ -62,7 +78,8 @@ export class GlobalSearch extends Component {
|
|
|
62
78
|
const groupId = `${id}-group-${groupIndex}`;
|
|
63
79
|
return `<li class="fi-global-search-result-group"><h3 id="${e(groupId)}" class="fi-global-search-result-group-header">${e(category.label)}</h3><ul class="fi-global-search-result-group-results">${category.results.map((result, index) => {
|
|
64
80
|
const details = Object.entries(result.details);
|
|
65
|
-
|
|
81
|
+
const actions = actionsHtml.get(result);
|
|
82
|
+
return `<li class="${actions ? 'fi-global-search-result fi-global-search-result-has-actions' : 'fi-global-search-result'}"><a href="${e(result.url)}" wire:navigate class="fi-global-search-result-link"><h4 id="${e(`${groupId}-result-${index}`)}" class="fi-global-search-result-heading">${e(result.title)}</h4>${details.length ? `<dl class="fi-global-search-result-details">${details.map(([label, value]) => `<div class="fi-global-search-result-detail"><dt class="fi-global-search-result-detail-label">${e(label)}:</dt><dd class="fi-global-search-result-detail-value">${e(value)}</dd></div>`).join('')}</dl>` : ''}</a>${actions ? `<div class="fi-global-search-result-actions">${actions.join('')}</div>` : ''}</li>`;
|
|
66
83
|
}).join('')}</ul></li>`;
|
|
67
84
|
}).join('')}</ul>`;
|
|
68
85
|
}
|
package/lib/totp.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** RFC 4648 base32, without padding (the format of authenticator app secrets). */
|
|
2
|
+
export declare function base32Encode(bytes: Uint8Array): string;
|
|
3
|
+
/** Decodes base32 (case, spaces and padding ignored). Throws on any other character. */
|
|
4
|
+
export declare function base32Decode(text: string): Buffer;
|
|
5
|
+
export type TotpAlgorithm = 'sha1' | 'sha256' | 'sha512';
|
|
6
|
+
export type TotpOptions = {
|
|
7
|
+
/** Seconds per code (30 by default, what every app expects). */
|
|
8
|
+
period?: number;
|
|
9
|
+
/** Code length (6 by default). */
|
|
10
|
+
digits?: number;
|
|
11
|
+
/** HMAC algorithm (`sha1` by default: most apps ignore any other value). */
|
|
12
|
+
algorithm?: TotpAlgorithm;
|
|
13
|
+
};
|
|
14
|
+
/** Random base32 secret of `bytes` bytes (20 by default: 160 bits, as RFC 4226 recommends). */
|
|
15
|
+
export declare function generateTotpSecret(bytes?: number): string;
|
|
16
|
+
/** HOTP value for a counter. */
|
|
17
|
+
export declare function hotp(secret: string | Uint8Array, counter: number, options?: Pick<TotpOptions, 'digits' | 'algorithm'>): string;
|
|
18
|
+
/** Time step (counter) of a moment: `floor(unix seconds / period)`. */
|
|
19
|
+
export declare const totpTimestep: (at?: number, period?: number) => number;
|
|
20
|
+
/** Current code of a secret (or the code at `at`, in milliseconds). */
|
|
21
|
+
export declare function totpCode(secret: string, options?: TotpOptions & {
|
|
22
|
+
at?: number;
|
|
23
|
+
}): string;
|
|
24
|
+
/**
|
|
25
|
+
* Checks a code against the secret, accepting `window` time steps before and after the current one (clock drift).
|
|
26
|
+
* Returns the matched time step, or `null`. Pass the last accepted step as `after` to refuse a code that was already
|
|
27
|
+
* used, or any older one (RFC 6238 §5.2): store the returned step and pass it next time.
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyTotp(secret: string, code: unknown, options?: TotpOptions & {
|
|
30
|
+
window?: number;
|
|
31
|
+
at?: number;
|
|
32
|
+
after?: number | null;
|
|
33
|
+
}): number | null;
|
|
34
|
+
/**
|
|
35
|
+
* `otpauth://` URI to show as a QR code: `issuer` is the application name, `account` the user's (their email).
|
|
36
|
+
* See https://github.com/google/google-authenticator/wiki/Key-Uri-Format.
|
|
37
|
+
*/
|
|
38
|
+
export declare function totpUri(secret: string, account: string, issuer: string, options?: TotpOptions): string;
|