mevebu 1.6.0 → 1.7.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.
- package/mevebu/app/tailwind.css +32 -2
- package/mevebu/app/tailwind.src.css +14 -2
- package/mevebu/libs/js/core.js +12 -5
- package/mevebu/libs/js/tools/flashcard-creator.js +17 -5
- package/mevebu/libs/js/tools/jwt-generator.js +61 -19
- package/mevebu/libs/js/tools/password-generator.js +2 -1
- package/package.json +1 -1
package/mevebu/app/tailwind.css
CHANGED
|
@@ -631,6 +631,9 @@
|
|
|
631
631
|
.grid-cols-1 {
|
|
632
632
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
633
633
|
}
|
|
634
|
+
.grid-cols-2 {
|
|
635
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
636
|
+
}
|
|
634
637
|
.grid-cols-\[minmax\(0\,1fr\)_auto\] {
|
|
635
638
|
grid-template-columns: minmax(0,1fr) auto;
|
|
636
639
|
}
|
|
@@ -824,6 +827,9 @@
|
|
|
824
827
|
--tw-border-style: dashed;
|
|
825
828
|
border-style: dashed;
|
|
826
829
|
}
|
|
830
|
+
.border-danger {
|
|
831
|
+
border-color: var(--danger);
|
|
832
|
+
}
|
|
827
833
|
.border-jade {
|
|
828
834
|
border-color: var(--jade);
|
|
829
835
|
}
|
|
@@ -851,6 +857,9 @@
|
|
|
851
857
|
.bg-black {
|
|
852
858
|
background-color: var(--color-black);
|
|
853
859
|
}
|
|
860
|
+
.bg-danger {
|
|
861
|
+
background-color: var(--danger);
|
|
862
|
+
}
|
|
854
863
|
.bg-deck {
|
|
855
864
|
background-color: var(--deck);
|
|
856
865
|
}
|
|
@@ -1229,6 +1238,9 @@
|
|
|
1229
1238
|
.text-jade {
|
|
1230
1239
|
color: var(--jade);
|
|
1231
1240
|
}
|
|
1241
|
+
.text-on-danger {
|
|
1242
|
+
color: var(--on-danger);
|
|
1243
|
+
}
|
|
1232
1244
|
.text-on-jade {
|
|
1233
1245
|
color: var(--on-jade);
|
|
1234
1246
|
}
|
|
@@ -1419,6 +1431,9 @@
|
|
|
1419
1431
|
border-color: var(--jade);
|
|
1420
1432
|
}
|
|
1421
1433
|
@media (hover: hover) {
|
|
1434
|
+
.hover\:border-danger-deep:hover {
|
|
1435
|
+
border-color: var(--danger-deep);
|
|
1436
|
+
}
|
|
1422
1437
|
.hover\:border-jade:hover {
|
|
1423
1438
|
border-color: var(--jade);
|
|
1424
1439
|
}
|
|
@@ -1428,6 +1443,9 @@
|
|
|
1428
1443
|
.hover\:border-orbit-hard:hover {
|
|
1429
1444
|
border-color: var(--orbit-hard);
|
|
1430
1445
|
}
|
|
1446
|
+
.hover\:bg-danger-deep:hover {
|
|
1447
|
+
background-color: var(--danger-deep);
|
|
1448
|
+
}
|
|
1431
1449
|
.hover\:bg-deck:hover {
|
|
1432
1450
|
background-color: var(--deck);
|
|
1433
1451
|
}
|
|
@@ -1651,6 +1669,11 @@
|
|
|
1651
1669
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1652
1670
|
}
|
|
1653
1671
|
}
|
|
1672
|
+
@media (width >= 80rem) {
|
|
1673
|
+
.min-\[80rem\]\:grid-cols-2 {
|
|
1674
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1654
1677
|
.\[\&_a\]\:text-jade a {
|
|
1655
1678
|
color: var(--jade);
|
|
1656
1679
|
}
|
|
@@ -1994,6 +2017,8 @@
|
|
|
1994
2017
|
--on-jade: #ffffff;
|
|
1995
2018
|
--jade-soft: #e0f0e7;
|
|
1996
2019
|
--danger: #b0231d;
|
|
2020
|
+
--danger-deep: #8e1b16;
|
|
2021
|
+
--on-danger: #ffffff;
|
|
1997
2022
|
--star: #b37700;
|
|
1998
2023
|
--track: #bfcac1;
|
|
1999
2024
|
--scrim: rgb(8 14 11 / 0.5);
|
|
@@ -2039,8 +2064,10 @@
|
|
|
2039
2064
|
--on-jade: #04120b;
|
|
2040
2065
|
--jade-soft: #10291f;
|
|
2041
2066
|
--danger: #ff8a80;
|
|
2067
|
+
--danger-deep: #ffb3ab;
|
|
2068
|
+
--on-danger: #2a0b08;
|
|
2042
2069
|
--star: #ffcf4d;
|
|
2043
|
-
--track: #
|
|
2070
|
+
--track: #3f5c49;
|
|
2044
2071
|
--scrim: rgb(2 6 4 / 0.68);
|
|
2045
2072
|
--shadow: 0 22px 50px -20px rgb(0 0 0 / 0.7);
|
|
2046
2073
|
--caret: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2385978a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
|
|
@@ -2075,7 +2102,10 @@
|
|
|
2075
2102
|
border-radius: 6px;
|
|
2076
2103
|
}
|
|
2077
2104
|
::selection {
|
|
2078
|
-
background: var(--jade
|
|
2105
|
+
background: var(--jade);
|
|
2106
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
2107
|
+
background: color-mix(in srgb, var(--jade) 34%, transparent);
|
|
2108
|
+
}
|
|
2079
2109
|
}
|
|
2080
2110
|
@media (prefers-reduced-motion: reduce) {
|
|
2081
2111
|
*, *::before, *::after {
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
--on-jade: #ffffff;
|
|
51
51
|
--jade-soft: #e0f0e7;
|
|
52
52
|
--danger: #b0231d;
|
|
53
|
+
--danger-deep: #8e1b16;
|
|
54
|
+
--on-danger: #ffffff;
|
|
53
55
|
--star: #b37700;
|
|
54
56
|
--track: #bfcac1;
|
|
55
57
|
--scrim: rgb(8 14 11 / 0.5);
|
|
@@ -106,8 +108,10 @@
|
|
|
106
108
|
--on-jade: #04120b;
|
|
107
109
|
--jade-soft: #10291f;
|
|
108
110
|
--danger: #ff8a80;
|
|
111
|
+
--danger-deep: #ffb3ab;
|
|
112
|
+
--on-danger: #2a0b08;
|
|
109
113
|
--star: #ffcf4d;
|
|
110
|
-
--track: #
|
|
114
|
+
--track: #3f5c49;
|
|
111
115
|
--scrim: rgb(2 6 4 / 0.68);
|
|
112
116
|
--shadow: 0 22px 50px -20px rgb(0 0 0 / 0.7);
|
|
113
117
|
--caret: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2385978a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
|
|
@@ -136,6 +140,8 @@
|
|
|
136
140
|
--color-on-jade: var(--on-jade);
|
|
137
141
|
--color-jade-soft: var(--jade-soft);
|
|
138
142
|
--color-danger: var(--danger);
|
|
143
|
+
--color-danger-deep: var(--danger-deep);
|
|
144
|
+
--color-on-danger: var(--on-danger);
|
|
139
145
|
--color-star: var(--star);
|
|
140
146
|
--color-track: var(--track);
|
|
141
147
|
|
|
@@ -201,7 +207,13 @@
|
|
|
201
207
|
border-radius: 6px;
|
|
202
208
|
}
|
|
203
209
|
|
|
204
|
-
|
|
210
|
+
/* A selection has to read on both grounds. The browser default nearly vanishes
|
|
211
|
+
on the dark deck, and `--jade-soft` is darker still there, so the highlight
|
|
212
|
+
is the accent mixed down instead, which stays visible over a field, a
|
|
213
|
+
readout and the page alike. */
|
|
214
|
+
::selection {
|
|
215
|
+
background: color-mix(in srgb, var(--jade) 34%, transparent);
|
|
216
|
+
}
|
|
205
217
|
|
|
206
218
|
@media (prefers-reduced-motion: reduce) {
|
|
207
219
|
*, *::before, *::after {
|
package/mevebu/libs/js/core.js
CHANGED
|
@@ -616,6 +616,7 @@ const BTN_KIND = {
|
|
|
616
616
|
fill: "border-jade bg-jade px-4 text-on-jade hover:border-jade-deep hover:bg-jade-deep",
|
|
617
617
|
plain: "border-orbit bg-deck px-4 text-ink hover:border-orbit-hard hover:bg-deck-2",
|
|
618
618
|
quiet: "border-transparent bg-transparent px-2.5 text-ink-2 hover:bg-deck-2 hover:text-ink",
|
|
619
|
+
danger: "border-danger bg-danger px-4 text-on-danger hover:border-danger-deep hover:bg-danger-deep",
|
|
619
620
|
};
|
|
620
621
|
|
|
621
622
|
export function button(labelText, { icon: iconName, kind = "plain", class: extra, tag = "button", ...rest } = {}) {
|
|
@@ -623,7 +624,9 @@ export function button(labelText, { icon: iconName, kind = "plain", class: extra
|
|
|
623
624
|
tag,
|
|
624
625
|
{ class: withExtra(`${BTN} ${BTN_KIND[kind] ?? BTN_KIND.plain}`, extra), ...(tag === "button" ? { type: "button" } : null), ...rest },
|
|
625
626
|
iconName ? icon(iconName) : null,
|
|
626
|
-
|
|
627
|
+
// An icon-only button skips the label span: an empty one still spends the
|
|
628
|
+
// row's gap, which pushes the icon off centre.
|
|
629
|
+
labelText ? el("span", {}, labelText) : null
|
|
627
630
|
);
|
|
628
631
|
}
|
|
629
632
|
|
|
@@ -681,10 +684,14 @@ export function chipRow(items, { value, onChange, label } = {}) {
|
|
|
681
684
|
return wrap;
|
|
682
685
|
}
|
|
683
686
|
|
|
684
|
-
/**
|
|
685
|
-
|
|
687
|
+
/**
|
|
688
|
+
* Independent toggles. Returns the wrapper with `.values()` reading the boxes.
|
|
689
|
+
* `columns: 2` lays them out on an even grid, so a set of related boxes lines up
|
|
690
|
+
* instead of ragging across the row.
|
|
691
|
+
*/
|
|
692
|
+
export function toggleRow(items, { columns } = {}) {
|
|
686
693
|
const boxes = new Map();
|
|
687
|
-
const wrap = el("div", { class: "flex flex-wrap gap-1.5" });
|
|
694
|
+
const wrap = el("div", { class: columns === 2 ? "grid grid-cols-2 gap-1.5" : "flex flex-wrap gap-1.5" });
|
|
688
695
|
for (const item of items) {
|
|
689
696
|
const node = checkbox(item.label, { checked: item.checked !== false });
|
|
690
697
|
boxes.set(item.value, node.input);
|
|
@@ -743,7 +750,7 @@ export function blank(iconName, text) {
|
|
|
743
750
|
// ---------- version ----------
|
|
744
751
|
|
|
745
752
|
// Kept in one place. The CLI reads the same value from package.json.
|
|
746
|
-
export const VERSION = "1.
|
|
753
|
+
export const VERSION = "1.7.0";
|
|
747
754
|
export const PACKAGE_NAME = "mevebu";
|
|
748
755
|
export const REPO_SLUG = "mevebu/mevebu";
|
|
749
756
|
|
|
@@ -34,7 +34,7 @@ export function mount(slot) {
|
|
|
34
34
|
JSON.stringify(cards.map(({ question: q, answer: a }) => ({ question: q, answer: a })), null, 2);
|
|
35
35
|
|
|
36
36
|
function removeButton(card) {
|
|
37
|
-
const node = button("", { icon: "trash", "aria-label": `Remove ${card.question}` });
|
|
37
|
+
const node = button("", { icon: "trash", kind: "danger", "aria-label": `Remove ${card.question}` });
|
|
38
38
|
node.addEventListener("click", () => {
|
|
39
39
|
cards = cards.filter((entry) => entry.id !== card.id);
|
|
40
40
|
if (index >= cards.length) index = Math.max(0, cards.length - 1);
|
|
@@ -52,10 +52,22 @@ export function mount(slot) {
|
|
|
52
52
|
const card = cards[index];
|
|
53
53
|
faceLabel.textContent = flipped ? "Answer" : "Question";
|
|
54
54
|
face.textContent = flipped ? card.answer : card.question;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// A row per card, not a table: the question takes the width it needs and
|
|
56
|
+
// the remove button stays on the centre line of every row.
|
|
57
|
+
listWrap.replaceChildren(
|
|
58
|
+
el(
|
|
59
|
+
"ol",
|
|
60
|
+
{ class: "m-0 grid list-none gap-1.5 p-0" },
|
|
61
|
+
...cards.map((entry, i) =>
|
|
62
|
+
el(
|
|
63
|
+
"li",
|
|
64
|
+
{ class: "flex items-center gap-3 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-2.5" },
|
|
65
|
+
el("span", { class: "min-w-0 flex-1 wrap-anywhere font-mono text-sm" }, `${i + 1}. ${entry.question}`),
|
|
66
|
+
removeButton(entry)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
);
|
|
59
71
|
}
|
|
60
72
|
stats.replaceChildren(tile("Cards", cards.length), tile("Card", cards.length ? `${index + 1} / ${cards.length}` : "-"));
|
|
61
73
|
flipButton.disabled = !cards.length;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
// Signs a JSON Web Token with
|
|
1
|
+
// Signs a JSON Web Token with HMAC. Checks that the JSON parses and shows a
|
|
2
2
|
// readable decode of the payload, so a typo surfaces before signing.
|
|
3
|
-
import { el, icon, bench, stack, panel, textArea, textInput, notice, copyButton, output, debounce, actions, toolcard } from "../core.js";
|
|
3
|
+
import { el, icon, bench, stack, panel, textArea, textInput, selectInput, notice, copyButton, output, button, debounce, actions, toolcard } from "../core.js";
|
|
4
4
|
|
|
5
5
|
const encoder = new TextEncoder();
|
|
6
6
|
|
|
7
|
+
// What can actually be signed here. The select is built from this, so the list
|
|
8
|
+
// and the signing call can never drift apart.
|
|
9
|
+
const ALGS = { HS256: "SHA-256", HS384: "SHA-384", HS512: "SHA-512" };
|
|
10
|
+
const TYPES = ["JWT", "at+jwt"];
|
|
11
|
+
|
|
7
12
|
// Only claims that carry a value the JSON does not show by itself: a Unix
|
|
8
13
|
// timestamp means nothing to read, the date behind it does.
|
|
9
14
|
const TIME_CLAIMS = [["iat", "Issued at"], ["nbf", "Not before"], ["exp", "Expires"]];
|
|
@@ -18,16 +23,14 @@ const toUrlString = (text) => toUrl(encoder.encode(text));
|
|
|
18
23
|
export function mount(slot) {
|
|
19
24
|
const now = Math.floor(Date.now() / 1000);
|
|
20
25
|
|
|
21
|
-
const headerField = textArea({ rows:
|
|
26
|
+
const headerField = textArea({ rows: 6, value: '{\n "alg": "HS256",\n "typ": "JWT"\n}' });
|
|
22
27
|
const payloadField = textArea({
|
|
23
|
-
rows:
|
|
28
|
+
rows: 6,
|
|
24
29
|
value: `{\n "sub": "1234567890",\n "name": "Ada",\n "admin": true,\n "iat": ${now}\n}`});
|
|
25
30
|
const secretField = textInput({ type: "password", value: "your-256-bit-secret", autocomplete: "new-password" });
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
icon("eye")
|
|
30
|
-
);
|
|
31
|
+
const algField = selectInput(Object.keys(ALGS), { value: "HS256", "aria-label": "Algorithm" });
|
|
32
|
+
const typField = selectInput(TYPES, { value: "JWT", "aria-label": "Token type" });
|
|
33
|
+
const showButton = button("", { icon: "eye", kind: "plain", "aria-label": "Show secret", "aria-pressed": "false" });
|
|
31
34
|
const error = notice("error");
|
|
32
35
|
const tokenOut = output({ tag: "textarea", rows: 4 });
|
|
33
36
|
const decoded = output({ rows: 4 });
|
|
@@ -43,6 +46,33 @@ export function mount(slot) {
|
|
|
43
46
|
showButton.setAttribute("aria-pressed", String(!visible));
|
|
44
47
|
});
|
|
45
48
|
|
|
49
|
+
/** The header as data, or null while it is mid edit. */
|
|
50
|
+
const readHeader = () => {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(headerField.value);
|
|
53
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A header typed by hand still moves the two selects when it names a known value. */
|
|
60
|
+
function followHeader() {
|
|
61
|
+
const header = readHeader();
|
|
62
|
+
if (!header) return;
|
|
63
|
+
if (ALGS[header.alg]) algField.value = header.alg;
|
|
64
|
+
if (TYPES.includes(header.typ)) typField.value = header.typ;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A select writes back into the header, which stays the one source of truth. */
|
|
68
|
+
function writeHeader() {
|
|
69
|
+
const header = readHeader() ?? {};
|
|
70
|
+
header.alg = algField.value;
|
|
71
|
+
header.typ = typField.value;
|
|
72
|
+
headerField.value = JSON.stringify(header, null, 2);
|
|
73
|
+
sign();
|
|
74
|
+
}
|
|
75
|
+
|
|
46
76
|
async function sign() {
|
|
47
77
|
error.write("");
|
|
48
78
|
let header;
|
|
@@ -54,8 +84,9 @@ export function mount(slot) {
|
|
|
54
84
|
error.write(`That JSON does not parse. ${caught.message}`);
|
|
55
85
|
return;
|
|
56
86
|
}
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
const hash = ALGS[header.alg];
|
|
88
|
+
if (!hash) {
|
|
89
|
+
error.write(`Only ${Object.keys(ALGS).join(", ")} can be signed here. Set alg to one of those.`);
|
|
59
90
|
return;
|
|
60
91
|
}
|
|
61
92
|
let token = "";
|
|
@@ -63,7 +94,7 @@ export function mount(slot) {
|
|
|
63
94
|
const first = toUrlString(JSON.stringify(header));
|
|
64
95
|
const second = toUrlString(JSON.stringify(payload));
|
|
65
96
|
const message = `${first}.${second}`;
|
|
66
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(secretField.value), { name: "HMAC", hash
|
|
97
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secretField.value), { name: "HMAC", hash }, false, ["sign"]);
|
|
67
98
|
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
|
|
68
99
|
token = `${message}.${toUrl(new Uint8Array(signature))}`;
|
|
69
100
|
} catch (caught) {
|
|
@@ -84,19 +115,30 @@ export function mount(slot) {
|
|
|
84
115
|
// Live signing. The textareas are the only slow field: invalid JSON while a
|
|
85
116
|
// half-typed brace is on screen is normal, so the wait is longer than usual.
|
|
86
117
|
const live = debounce(sign, 250);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
headerField.addEventListener("input", () => { followHeader(); live(); });
|
|
119
|
+
for (const field of [payloadField, secretField]) field.addEventListener("input", live);
|
|
120
|
+
for (const field of [algField, typField]) field.addEventListener("change", writeHeader);
|
|
90
121
|
|
|
91
122
|
const card = toolcard({
|
|
92
123
|
body: bench(
|
|
93
124
|
stack(
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
// The secret leads the column: it is the one field a visitor has to fill
|
|
126
|
+
// before the token means anything, and the two header choices are the
|
|
127
|
+
// adjustment after it.
|
|
96
128
|
panel("Secret", el("div", { class: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2" }, secretField, showButton)),
|
|
129
|
+
panel("Algorithm", algField),
|
|
130
|
+
panel("Type", typField),
|
|
97
131
|
error
|
|
98
132
|
),
|
|
99
|
-
stack(
|
|
133
|
+
stack(
|
|
134
|
+
// Header and payload sit side by side once there is room for two columns,
|
|
135
|
+
// which is the pair a reader compares anyway.
|
|
136
|
+
el("div", { class: "grid gap-4 min-[80rem]:grid-cols-2" },
|
|
137
|
+
panel("Header", headerField),
|
|
138
|
+
panel("Payload", payloadField)),
|
|
139
|
+
panel("Token", tokenOut),
|
|
140
|
+
timePanel
|
|
141
|
+
)
|
|
100
142
|
),
|
|
101
143
|
foot: actions(copyBtn)});
|
|
102
144
|
slot.append(card.node);
|
|
@@ -105,4 +147,4 @@ export function mount(slot) {
|
|
|
105
147
|
return {};
|
|
106
148
|
}
|
|
107
149
|
|
|
108
|
-
export default { mount };
|
|
150
|
+
export default { mount };
|
|
@@ -15,11 +15,12 @@ export function mount(slot) {
|
|
|
15
15
|
lengthLabel.textContent = `Length: ${lengthField.value}`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Two columns, so the four groups read as a block instead of ragging.
|
|
18
19
|
const groups = toggleRow([
|
|
19
20
|
{ value: "lower", label: "a to z" },
|
|
20
21
|
{ value: "upper", label: "A to Z" },
|
|
21
22
|
{ value: "digits", label: "0 to 9" },
|
|
22
|
-
{ value: "symbols", label: "Symbols" }]);
|
|
23
|
+
{ value: "symbols", label: "Symbols" }], { columns: 2 });
|
|
23
24
|
|
|
24
25
|
const secret = textInput({ mono: true, readonly: true, "aria-label": "Password", placeholder: "Press Generate" });
|
|
25
26
|
const meter = el("div", { class: "h-2 overflow-hidden rounded-full bg-deck-2", role: "img", "aria-label": "Strength" }, el("div", { class: "h-full w-0 rounded-full bg-orbit-hard transition-[width,background-color] duration-150 data-[level=weak]:bg-danger data-[level=ok]:bg-star data-[level=strong]:bg-jade" }));
|