mevebu 2.2.0 → 2.4.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/README.md +8 -8
- package/mevebu/app/tailwind.css +12 -0
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/js-tokens.js +195 -0
- package/mevebu/libs/js/registry.js +180 -0
- package/mevebu/libs/js/tools/bcrypt-hash.js +161 -0
- package/mevebu/libs/js/tools/javascript-formatter.js +182 -0
- package/mevebu/libs/js/tools/javascript-minifier.js +111 -0
- package/mevebu/libs/js/tools/json-to-yaml.js +59 -0
- package/mevebu/libs/js/tools/password-policy.js +139 -0
- package/mevebu/libs/js/tools/rsa-key-pair.js +137 -0
- package/mevebu/libs/js/tools/secret-scanner.js +168 -0
- package/mevebu/libs/js/tools/ssh-fingerprint.js +210 -0
- package/mevebu/libs/js/tools/xml-minifier.js +103 -0
- package/mevebu/libs/js/tools/yaml-to-json.js +46 -0
- package/mevebu/libs/js/vendor/bcrypt.min.js +1220 -0
- package/mevebu/libs/js/vendor/bcryptjs.LICENSE +27 -0
- package/mevebu/libs/js/vendor/js-yaml.LICENSE +21 -0
- package/mevebu/libs/js/vendor/js-yaml.mjs +3851 -0
- package/package.json +2 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Formats JavaScript by re-indenting its tokens. Strings, templates, regexes
|
|
2
|
+
// and comments are kept byte for byte.
|
|
3
|
+
import { el, bench, stack, panel, textArea, notice, chipRow, copyButton, actions, toolcard } from "../core.js";
|
|
4
|
+
import { tokenize } from "../js-tokens.js";
|
|
5
|
+
|
|
6
|
+
const CONTROL = new Set(["if", "for", "while", "switch", "catch", "with"]);
|
|
7
|
+
const BINARY = new Set([
|
|
8
|
+
"=", "+", "-", "*", "/", "%", "**", "==", "===", "!=", "!==", "<", ">", "<=", ">=",
|
|
9
|
+
"&&", "||", "??", "&", "|", "^", "<<", ">>", ">>>", "+=", "-=", "*=", "/=", "%=",
|
|
10
|
+
"**=", "&=", "|=", "^=", "<<=", ">>=", ">>>=", "&&=", "||=", "??=", "=>",
|
|
11
|
+
]);
|
|
12
|
+
const NO_SPACE_BEFORE = new Set([")", "]", ",", ";", ".", "?.", "++", "--"]);
|
|
13
|
+
const VALUE_TYPES = new Set(["word", "number", "string", "template", "regex"]);
|
|
14
|
+
|
|
15
|
+
function bracketError(tokens) {
|
|
16
|
+
const stack = [];
|
|
17
|
+
const pairs = { ")": "(", "]": "[", "}": "{" };
|
|
18
|
+
for (const token of tokens) {
|
|
19
|
+
if (token.type !== "punct") continue;
|
|
20
|
+
if ("([{".includes(token.value)) stack.push(token.value);
|
|
21
|
+
else if (")]}".includes(token.value)) {
|
|
22
|
+
if (stack.pop() !== pairs[token.value]) return `Unexpected ${token.value}.`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return stack.length ? `Unclosed ${stack[stack.length - 1]}.` : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function format(source, size) {
|
|
29
|
+
const { tokens, error } = tokenize(source);
|
|
30
|
+
if (error) return { error };
|
|
31
|
+
const balance = bracketError(tokens);
|
|
32
|
+
if (balance) return { error: balance };
|
|
33
|
+
|
|
34
|
+
const pad = " ".repeat(size);
|
|
35
|
+
const lines = [];
|
|
36
|
+
let line = "";
|
|
37
|
+
let indent = 0;
|
|
38
|
+
let paren = 0;
|
|
39
|
+
|
|
40
|
+
const flush = () => {
|
|
41
|
+
if (line.trim()) lines.push(pad.repeat(indent) + line.trimEnd());
|
|
42
|
+
line = "";
|
|
43
|
+
};
|
|
44
|
+
const add = (text, space = false) => {
|
|
45
|
+
if (space && line && !line.endsWith(" ")) line += " ";
|
|
46
|
+
line += text;
|
|
47
|
+
};
|
|
48
|
+
const nextOf = (index) => tokens[index + 1] ?? null;
|
|
49
|
+
|
|
50
|
+
tokens.forEach((token, index) => {
|
|
51
|
+
const prev = tokens[index - 1] ?? null;
|
|
52
|
+
const next = nextOf(index);
|
|
53
|
+
const value = token.value;
|
|
54
|
+
|
|
55
|
+
if (token.type === "lineComment") {
|
|
56
|
+
add(value, Boolean(line));
|
|
57
|
+
flush();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (token.type === "blockComment") {
|
|
61
|
+
add(value, Boolean(line));
|
|
62
|
+
if (value.includes("\n")) flush();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (token.type === "punct") {
|
|
67
|
+
if (value === "{") {
|
|
68
|
+
const space = prev && (
|
|
69
|
+
prev.type === "word" || prev.type === "number" || prev.type === "string" ||
|
|
70
|
+
prev.type === "template" || prev.value === ")" || prev.value === "]"
|
|
71
|
+
);
|
|
72
|
+
add("{", space);
|
|
73
|
+
flush();
|
|
74
|
+
indent++;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (value === "}") {
|
|
78
|
+
flush();
|
|
79
|
+
indent = Math.max(0, indent - 1);
|
|
80
|
+
line = "}";
|
|
81
|
+
const keep = next && (
|
|
82
|
+
next.value === ";" || next.value === "," || next.value === ")" ||
|
|
83
|
+
next.value === "]" || next.value === "else" || next.value === "catch" ||
|
|
84
|
+
next.value === "finally" || next.value === "while"
|
|
85
|
+
);
|
|
86
|
+
if (!keep) flush();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (value === ";") {
|
|
90
|
+
add(";");
|
|
91
|
+
if (paren === 0) flush();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (value === "(") {
|
|
95
|
+
const control = prev && prev.type === "word" && CONTROL.has(prev.value);
|
|
96
|
+
add("(", control);
|
|
97
|
+
paren++;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (value === ")") {
|
|
101
|
+
add(")");
|
|
102
|
+
paren = Math.max(0, paren - 1);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (value === "[") { add("["); return; }
|
|
106
|
+
if (value === "]") { add("]"); return; }
|
|
107
|
+
if (value === ",") { add(","); add(" "); return; }
|
|
108
|
+
if (value === ":") {
|
|
109
|
+
add(":");
|
|
110
|
+
add(" ");
|
|
111
|
+
if (line.trimStart().startsWith("case ") || line.trimStart().startsWith("default")) flush();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (value === "?") { add(" ? "); return; }
|
|
115
|
+
if (value === "." || value === "?." || value === "++" || value === "--" || value === "!" || value === "~" || value === "...") {
|
|
116
|
+
add(value);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (BINARY.has(value)) {
|
|
120
|
+
add(` ${value} `);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
add(value);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const needsSpace = prev && (
|
|
128
|
+
VALUE_TYPES.has(prev.type) || prev.value === ")" || prev.value === "]" || prev.value === "}"
|
|
129
|
+
);
|
|
130
|
+
add(value, needsSpace);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
flush();
|
|
134
|
+
return { text: lines.join("\n") };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function mount(slot) {
|
|
138
|
+
const source = textArea({
|
|
139
|
+
rows: 14,
|
|
140
|
+
"aria-label": "JavaScript",
|
|
141
|
+
value: "const greet=(name)=>{if(!name)return 'hello';return `hello ${name}`};\n// keep this comment\nconst list=[1,2,3].map(n=>n*2);",
|
|
142
|
+
});
|
|
143
|
+
const result = textArea({ rows: 14, readonly: true, "aria-label": "Formatted JavaScript" });
|
|
144
|
+
const error = notice("error");
|
|
145
|
+
let size = "2";
|
|
146
|
+
|
|
147
|
+
const chips = chipRow(
|
|
148
|
+
[{ value: "2", label: "2 spaces" }, { value: "4", label: "4 spaces" }],
|
|
149
|
+
{ value: "2", label: "Indent", onChange: (value) => { size = value; run(); } },
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
function run() {
|
|
153
|
+
error.write("");
|
|
154
|
+
if (!source.value.trim()) {
|
|
155
|
+
result.value = "";
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const output = format(source.value, Number(size));
|
|
159
|
+
if (output.error) {
|
|
160
|
+
result.value = "";
|
|
161
|
+
error.write(output.error);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
result.value = output.text;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
source.addEventListener("input", run);
|
|
168
|
+
|
|
169
|
+
const card = toolcard({
|
|
170
|
+
body: bench(
|
|
171
|
+
stack(panel("Indent", chips), panel("JavaScript", source), error),
|
|
172
|
+
stack(panel("Formatted", result)),
|
|
173
|
+
),
|
|
174
|
+
foot: actions(copyButton(() => result.value, "Copy JavaScript", "fill")),
|
|
175
|
+
});
|
|
176
|
+
slot.append(card.node);
|
|
177
|
+
run();
|
|
178
|
+
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default { mount };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Removes comments and redundant whitespace from JavaScript, without renaming
|
|
2
|
+
// anything or changing the meaning of a statement.
|
|
3
|
+
import { el, bench, stack, panel, textArea, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
4
|
+
import { tokenize, bytes } from "../js-tokens.js";
|
|
5
|
+
|
|
6
|
+
const VALUE_TYPES = new Set(["word", "number", "string", "template", "regex"]);
|
|
7
|
+
const ASI_WORDS = new Set(["return", "throw", "break", "continue", "yield"]);
|
|
8
|
+
const RISKY_START = new Set(["(", "[", "`", "+", "-", "/"]);
|
|
9
|
+
|
|
10
|
+
const tile = (key, value) =>
|
|
11
|
+
el("div", { class: "grid gap-0.5 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3" },
|
|
12
|
+
el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, value),
|
|
13
|
+
el("span", { class: "text-2xs font-semibold text-ink-3" }, key));
|
|
14
|
+
|
|
15
|
+
function keepComment(value) {
|
|
16
|
+
return value.startsWith("/*!") || value.startsWith("//!") || /@license|@preserve/.test(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function minify(source) {
|
|
20
|
+
const { tokens, error } = tokenize(source);
|
|
21
|
+
if (error) return { error };
|
|
22
|
+
|
|
23
|
+
let out = "";
|
|
24
|
+
let previous = null;
|
|
25
|
+
|
|
26
|
+
const emit = (text) => { out += text; };
|
|
27
|
+
|
|
28
|
+
tokens.forEach((token) => {
|
|
29
|
+
if (token.type === "lineComment" || token.type === "blockComment") {
|
|
30
|
+
if (keepComment(token.value)) {
|
|
31
|
+
if (out && !out.endsWith("\n")) emit("\n");
|
|
32
|
+
emit(token.value);
|
|
33
|
+
if (token.type === "lineComment") emit("\n");
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (previous) {
|
|
39
|
+
const newline = token.newline && (
|
|
40
|
+
(previous.type === "word" && ASI_WORDS.has(previous.value)) ||
|
|
41
|
+
(RISKY_START.has(token.value) && (
|
|
42
|
+
VALUE_TYPES.has(previous.type) || [")", "]", "}", "++", "--"].includes(previous.value)
|
|
43
|
+
))
|
|
44
|
+
);
|
|
45
|
+
if (newline) {
|
|
46
|
+
emit("\n");
|
|
47
|
+
emit(token.value);
|
|
48
|
+
previous = token;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const wordPair = VALUE_TYPES.has(previous.type) && VALUE_TYPES.has(token.type);
|
|
53
|
+
const plusPair = ["+", "++"].includes(previous.value) && ["+", "++"].includes(token.value);
|
|
54
|
+
const minusPair = ["-", "--"].includes(previous.value) && ["-", "--"].includes(token.value);
|
|
55
|
+
const commentStart = previous.value === "/" && ["/", "*"].includes(token.value);
|
|
56
|
+
if (wordPair || plusPair || minusPair || commentStart) emit(" ");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
emit(token.value);
|
|
60
|
+
previous = token;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return { text: out.trim() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function mount(slot) {
|
|
67
|
+
const source = textArea({
|
|
68
|
+
rows: 14,
|
|
69
|
+
"aria-label": "JavaScript",
|
|
70
|
+
value: "// greeting\nconst greet = (name) => {\n if (!name) return 'hello';\n return `hello ${name}`;\n};\n\nconst list = [1, 2, 3].map((n) => n * 2);",
|
|
71
|
+
});
|
|
72
|
+
const result = output({ tag: "textarea", rows: 12 });
|
|
73
|
+
const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" });
|
|
74
|
+
const error = notice("error");
|
|
75
|
+
|
|
76
|
+
function run() {
|
|
77
|
+
error.write("");
|
|
78
|
+
stats.replaceChildren();
|
|
79
|
+
if (!source.value.trim()) {
|
|
80
|
+
result.value = "";
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const output = minify(source.value);
|
|
84
|
+
if (output.error) {
|
|
85
|
+
result.value = "";
|
|
86
|
+
error.write(output.error);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const before = bytes(source.value);
|
|
90
|
+
const after = bytes(output.text);
|
|
91
|
+
const saved = before ? Math.max(0, Math.round((1 - after / before) * 100)) : 0;
|
|
92
|
+
result.value = output.text;
|
|
93
|
+
stats.append(tile("Before", `${before} B`), tile("After", `${after} B`), tile("Saved", `${saved}%`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
source.addEventListener("input", run);
|
|
97
|
+
|
|
98
|
+
const card = toolcard({
|
|
99
|
+
body: bench(
|
|
100
|
+
stack(panel("JavaScript", source), error),
|
|
101
|
+
stack(panel("Minified", result), stats),
|
|
102
|
+
),
|
|
103
|
+
foot: actions(copyButton(() => result.value, "Copy result", "fill")),
|
|
104
|
+
});
|
|
105
|
+
slot.append(card.node);
|
|
106
|
+
run();
|
|
107
|
+
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default { mount };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Converts JSON to YAML with a vendored writer. Block style by default, flow
|
|
2
|
+
// style on request.
|
|
3
|
+
import { bench, stack, panel, textArea, output, notice, chipRow, downloadButton, copyButton, actions, toolcard } from "../core.js";
|
|
4
|
+
import { dump } from "../vendor/js-yaml.mjs";
|
|
5
|
+
|
|
6
|
+
const EXAMPLE = '{\n "service": "api",\n "replicas": 2,\n "ports": [8080, 8443],\n "labels": { "tier": "backend" }\n}';
|
|
7
|
+
|
|
8
|
+
export function mount(slot) {
|
|
9
|
+
const source = textArea({ rows: 14, "aria-label": "JSON", value: EXAMPLE });
|
|
10
|
+
const result = output({ tag: "textarea", rows: 12 });
|
|
11
|
+
const error = notice("error");
|
|
12
|
+
let style = "block";
|
|
13
|
+
|
|
14
|
+
const chips = chipRow(
|
|
15
|
+
[{ value: "block", label: "Block" }, { value: "flow", label: "Flow" }],
|
|
16
|
+
{ value: "block", label: "Style", onChange: (value) => { style = value; run(); } },
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
function run() {
|
|
20
|
+
error.write("");
|
|
21
|
+
if (!source.value.trim()) {
|
|
22
|
+
result.value = "";
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let value;
|
|
26
|
+
try {
|
|
27
|
+
value = JSON.parse(source.value);
|
|
28
|
+
} catch (caught) {
|
|
29
|
+
result.value = "";
|
|
30
|
+
error.write(caught.message);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
result.value = dump(value, {
|
|
34
|
+
indent: 2,
|
|
35
|
+
lineWidth: -1,
|
|
36
|
+
noRefs: true,
|
|
37
|
+
flowLevel: style === "flow" ? 0 : -1,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
source.addEventListener("input", run);
|
|
42
|
+
|
|
43
|
+
const card = toolcard({
|
|
44
|
+
body: bench(
|
|
45
|
+
stack(panel("Style", chips), panel("JSON", source), error),
|
|
46
|
+
stack(panel("YAML", result)),
|
|
47
|
+
),
|
|
48
|
+
foot: actions(
|
|
49
|
+
downloadButton(() => result.value, "data.yaml", "Download", "text/yaml"),
|
|
50
|
+
copyButton(() => result.value, "Copy YAML", "fill"),
|
|
51
|
+
),
|
|
52
|
+
});
|
|
53
|
+
slot.append(card.node);
|
|
54
|
+
run();
|
|
55
|
+
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default { mount };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Turns a written password rule into the three things a team needs: the rule in
|
|
2
|
+
// plain words, a regex that enforces it, and a checklist that tests a sample.
|
|
3
|
+
// The regex is built from the same settings as the text, so the two never drift.
|
|
4
|
+
import {
|
|
5
|
+
el, toast, bench, stack, panel, numberInput, checkbox, textInput, button,
|
|
6
|
+
actions, toolcard, notice, copyButton, output,
|
|
7
|
+
} from "../core.js";
|
|
8
|
+
|
|
9
|
+
function escapeClass(chars) {
|
|
10
|
+
return chars.replace(/[\]\\^-]/g, "\\$&");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function mount(slot) {
|
|
14
|
+
const minimum = numberInput({ min: "1", max: "128", step: "1", value: "12", "aria-label": "Minimum length" });
|
|
15
|
+
const maximum = numberInput({ min: "0", max: "512", step: "1", value: "0", "aria-label": "Maximum length, 0 for none" });
|
|
16
|
+
const lower = checkbox("Lowercase letters", { checked: true });
|
|
17
|
+
const upper = checkbox("Uppercase letters", { checked: true });
|
|
18
|
+
const digits = checkbox("Digits", { checked: true });
|
|
19
|
+
const symbols = checkbox("Symbols", { checked: false });
|
|
20
|
+
const noSpaces = checkbox("No spaces", { checked: true });
|
|
21
|
+
const noRepeat = numberInput({ min: "0", max: "10", step: "1", value: "0", "aria-label": "Max repeats in a row, 0 for none" });
|
|
22
|
+
const symbolSet = textInput({ value: "!@#$%^&*()-_=+[]{};:,.?", "aria-label": "Allowed symbols", class: "font-mono text-sm" });
|
|
23
|
+
const sample = textInput({ placeholder: "Type a password to test it", "aria-label": "Sample password", class: "font-mono", autocomplete: "off" });
|
|
24
|
+
|
|
25
|
+
const error = notice("warn");
|
|
26
|
+
const rule = el("p", { class: "m-0 text-sm leading-[1.6] text-ink" });
|
|
27
|
+
const pattern = output({ rows: 2 });
|
|
28
|
+
const checks = el("div", { class: "grid gap-1.5" });
|
|
29
|
+
const verdict = el("p", { class: "m-0 text-sm font-semibold" });
|
|
30
|
+
|
|
31
|
+
function classes() {
|
|
32
|
+
const out = [];
|
|
33
|
+
if (lower.input.checked) out.push({ label: "lowercase letter", test: (s) => /[a-z]/.test(s), rx: "(?=.*[a-z])" });
|
|
34
|
+
if (upper.input.checked) out.push({ label: "uppercase letter", test: (s) => /[A-Z]/.test(s), rx: "(?=.*[A-Z])" });
|
|
35
|
+
if (digits.input.checked) out.push({ label: "digit", test: (s) => /\d/.test(s), rx: "(?=.*\\d)" });
|
|
36
|
+
if (symbols.input.checked) {
|
|
37
|
+
const set = symbolSet.value;
|
|
38
|
+
const safe = escapeClass(set);
|
|
39
|
+
out.push({
|
|
40
|
+
label: `symbol from ${set || "the set"}`,
|
|
41
|
+
test: (s) => [...s].some((char) => set.includes(char)),
|
|
42
|
+
rx: `(?=.*[${safe}])`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function build() {
|
|
49
|
+
error.write("");
|
|
50
|
+
const min = Math.max(1, Number(minimum.value) || 1);
|
|
51
|
+
const max = Number(maximum.value) || 0;
|
|
52
|
+
const kinds = classes();
|
|
53
|
+
const repeat = Number(noRepeat.value) || 0;
|
|
54
|
+
|
|
55
|
+
if (max && max < min) error.write("The maximum is below the minimum, so no password can pass.");
|
|
56
|
+
if (!kinds.length && !noSpaces.input.checked) error.write("Pick at least one character class, or the rule accepts anything.");
|
|
57
|
+
|
|
58
|
+
const parts = [`at least ${min} characters`];
|
|
59
|
+
if (max) parts.push(`at most ${max} characters`);
|
|
60
|
+
for (const kind of kinds) parts.push(`at least one ${kind.label}`);
|
|
61
|
+
if (noSpaces.input.checked) parts.push("no spaces");
|
|
62
|
+
if (repeat) parts.push(`no more than ${repeat} identical characters in a row`);
|
|
63
|
+
rule.textContent = `A password needs ${listSentence(parts)}.`;
|
|
64
|
+
|
|
65
|
+
const lookaheads = kinds.map((kind) => kind.rx).join("");
|
|
66
|
+
const length = max ? `{${min},${max}}` : `{${min},}`;
|
|
67
|
+
const space = noSpaces.input.checked ? "(?!.*\\s)" : "";
|
|
68
|
+
const repeats = repeat ? `(?!.*(.)\\1{${repeat}})` : "";
|
|
69
|
+
const regex = `^${lookaheads}${space}${repeats}.${length}$`;
|
|
70
|
+
pattern.write(regex);
|
|
71
|
+
|
|
72
|
+
checks.replaceChildren();
|
|
73
|
+
const samples = sample.value;
|
|
74
|
+
const lengthOk = samples.length >= min && (!max || samples.length <= max);
|
|
75
|
+
const rows = [
|
|
76
|
+
{ label: `${min}${max ? `–${max}` : "+"} characters`, ok: samples.length > 0 ? lengthOk : null },
|
|
77
|
+
...kinds.map((kind) => ({ label: kind.label, ok: samples ? kind.test(samples) : null })),
|
|
78
|
+
];
|
|
79
|
+
if (noSpaces.input.checked) rows.push({ label: "no spaces", ok: samples ? !/\s/.test(samples) : null });
|
|
80
|
+
if (repeat) rows.push({ label: `no more than ${repeat} repeats in a row`, ok: samples ? !new RegExp(`(.)\\1{${repeat}}`).test(samples) : null });
|
|
81
|
+
|
|
82
|
+
checks.replaceChildren(
|
|
83
|
+
...rows.map((row) =>
|
|
84
|
+
el("div", { class: "flex items-center gap-2 text-sm" },
|
|
85
|
+
el("span", {
|
|
86
|
+
class: `inline-flex size-5 flex-none items-center justify-center rounded-full text-2xs font-bold ${row.ok === null ? "bg-deck-2 text-ink-3" : row.ok ? "bg-jade-soft text-jade" : "bg-danger/15 text-danger"}`,
|
|
87
|
+
}, row.ok === null ? "·" : row.ok ? "✓" : "×"),
|
|
88
|
+
el("span", { class: row.ok === false ? "text-danger" : "text-ink-2" }, row.label)))
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!samples) verdict.textContent = "";
|
|
92
|
+
else {
|
|
93
|
+
const failed = rows.filter((row) => row.ok === false).length;
|
|
94
|
+
verdict.textContent = failed === 0 ? "This password passes the rule." : `This password fails ${failed} part${failed === 1 ? "" : "s"} of the rule.`;
|
|
95
|
+
verdict.className = `m-0 text-sm font-semibold ${failed === 0 ? "text-jade" : "text-danger"}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function listSentence(items) {
|
|
100
|
+
if (items.length <= 1) return items.join("");
|
|
101
|
+
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const node of [minimum, maximum, noRepeat, symbolSet, sample]) node.addEventListener("input", build);
|
|
105
|
+
for (const node of [lower, upper, digits, symbols, noSpaces]) node.input.addEventListener("change", build);
|
|
106
|
+
|
|
107
|
+
const copyRule = copyButton(() => `${rule.textContent}\nRegex: ${pattern.textContent}`, "Copy policy");
|
|
108
|
+
|
|
109
|
+
build();
|
|
110
|
+
|
|
111
|
+
const shell = toolcard({
|
|
112
|
+
body: bench(
|
|
113
|
+
stack(
|
|
114
|
+
el("div", { class: "grid grid-cols-2 gap-3" }, panel("Minimum", minimum), panel("Maximum", maximum)),
|
|
115
|
+
el("div", { class: "grid gap-1.5" },
|
|
116
|
+
el("span", { class: "text-sm font-semibold text-ink-2" }, "Character classes"),
|
|
117
|
+
el("div", { class: "grid grid-cols-2 gap-2" }, lower, upper, digits, symbols),
|
|
118
|
+
noSpaces),
|
|
119
|
+
panel("Allowed symbols", symbolSet),
|
|
120
|
+
panel("Max repeats in a row", noRepeat, "0 means no limit."),
|
|
121
|
+
error
|
|
122
|
+
),
|
|
123
|
+
stack(
|
|
124
|
+
el("div", { class: "grid gap-2 rounded-r2 border border-orbit bg-deck-2 p-4" }, rule),
|
|
125
|
+
panel("Regex", pattern),
|
|
126
|
+
panel("Test a password", sample, "Shown in the page only. Nothing is sent."),
|
|
127
|
+
checks,
|
|
128
|
+
verdict
|
|
129
|
+
)
|
|
130
|
+
),
|
|
131
|
+
foot: actions(copyRule),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
slot.append(shell.node);
|
|
135
|
+
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default { mount };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// An RSA key pair made in the tab with Web Crypto. The keys exist only in the
|
|
2
|
+
// page until it closes, and both come out as PEM, the form most tools expect.
|
|
3
|
+
import {
|
|
4
|
+
el, toast, download, bench, stack, panel, selectInput, button, actions,
|
|
5
|
+
toolcard, blank, notice, copyButton, output, checkbox,
|
|
6
|
+
} from "../core.js";
|
|
7
|
+
|
|
8
|
+
const SIZES = [
|
|
9
|
+
{ value: "2048", label: "2048 bits" },
|
|
10
|
+
{ value: "3072", label: "3072 bits" },
|
|
11
|
+
{ value: "4096", label: "4096 bits" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function wrap(base64) {
|
|
15
|
+
return base64.match(/.{1,64}/g).join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toPem(label, der) {
|
|
19
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array(der)));
|
|
20
|
+
return `-----BEGIN ${label}-----\n${wrap(base64)}\n-----END ${label}-----\n`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function mount(slot) {
|
|
24
|
+
let pair = null;
|
|
25
|
+
let busy = false;
|
|
26
|
+
let token = 0;
|
|
27
|
+
|
|
28
|
+
const size = selectInput(SIZES, { value: "2048", "aria-label": "Key size" });
|
|
29
|
+
const showPrivate = checkbox("Show the private key", { checked: false });
|
|
30
|
+
|
|
31
|
+
const error = notice("warn");
|
|
32
|
+
const summary = el("p", { class: "text-sm text-ink-2" });
|
|
33
|
+
const publicOut = output({ rows: 8 });
|
|
34
|
+
const privateOut = output({ rows: 10 });
|
|
35
|
+
const empty = blank("key-round", "Generate a pair to see the keys.");
|
|
36
|
+
const publicBox = el("div", { class: "grid gap-1.5" },
|
|
37
|
+
el("span", { class: "text-2xs font-bold tracking-[0.06em] text-ink-3 uppercase" }, "Public key"), publicOut);
|
|
38
|
+
const privateBox = el("div", { class: "grid gap-1.5" },
|
|
39
|
+
el("span", { class: "text-2xs font-bold tracking-[0.06em] text-ink-3 uppercase" }, "Private key"), privateOut);
|
|
40
|
+
|
|
41
|
+
const generateButton = button("Generate pair", { icon: "refresh-cw", kind: "fill", onclick: generate });
|
|
42
|
+
const copyPublic = copyButton(() => pair?.publicPem ?? "", "Copy public");
|
|
43
|
+
const copyPrivate = copyButton(() => pair?.privatePem ?? "", "Copy private");
|
|
44
|
+
const downloadPublic = button("Download public", {
|
|
45
|
+
icon: "download",
|
|
46
|
+
onclick: () => { if (pair) download("public.pem", pair.publicPem, "application/x-pem-file"); },
|
|
47
|
+
});
|
|
48
|
+
const downloadPrivate = button("Download private", {
|
|
49
|
+
icon: "download",
|
|
50
|
+
onclick: () => { if (pair) download("private.pem", pair.privatePem, "application/x-pem-file"); },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function sync() {
|
|
54
|
+
const hasKeys = Boolean(pair);
|
|
55
|
+
publicBox.hidden = !hasKeys;
|
|
56
|
+
privateBox.hidden = !hasKeys || !showPrivate.input.checked;
|
|
57
|
+
empty.hidden = hasKeys;
|
|
58
|
+
privateOut.hidden = privateBox.hidden;
|
|
59
|
+
copyPrivate.disabled = !hasKeys;
|
|
60
|
+
copyPublic.disabled = !hasKeys;
|
|
61
|
+
downloadPrivate.disabled = !hasKeys;
|
|
62
|
+
downloadPublic.disabled = !hasKeys;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function generate() {
|
|
66
|
+
if (busy) return;
|
|
67
|
+
busy = true;
|
|
68
|
+
const current = ++token;
|
|
69
|
+
generateButton.disabled = true;
|
|
70
|
+
summary.textContent = "Generating. Larger keys take a few seconds.";
|
|
71
|
+
error.write("");
|
|
72
|
+
try {
|
|
73
|
+
const keys = await crypto.subtle.generateKey(
|
|
74
|
+
{
|
|
75
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
76
|
+
modulusLength: Number(size.value),
|
|
77
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
78
|
+
hash: "SHA-256",
|
|
79
|
+
},
|
|
80
|
+
true,
|
|
81
|
+
["sign", "verify"]
|
|
82
|
+
);
|
|
83
|
+
const spki = await crypto.subtle.exportKey("spki", keys.publicKey);
|
|
84
|
+
const pkcs8 = await crypto.subtle.exportKey("pkcs8", keys.privateKey);
|
|
85
|
+
if (current !== token) return;
|
|
86
|
+
pair = {
|
|
87
|
+
bits: Number(size.value),
|
|
88
|
+
publicPem: toPem("PUBLIC KEY", spki),
|
|
89
|
+
privatePem: toPem("PRIVATE KEY", pkcs8),
|
|
90
|
+
};
|
|
91
|
+
publicOut.write(pair.publicPem);
|
|
92
|
+
privateOut.write(pair.privatePem);
|
|
93
|
+
summary.textContent = `${pair.bits}-bit RSA · SHA-256 · exponent 65537 · PKCS#8 private, SPKI public`;
|
|
94
|
+
sync();
|
|
95
|
+
toast("Key pair ready");
|
|
96
|
+
} catch (caught) {
|
|
97
|
+
if (current !== token) return;
|
|
98
|
+
error.write(caught?.message || "The browser could not generate this key.");
|
|
99
|
+
} finally {
|
|
100
|
+
busy = false;
|
|
101
|
+
generateButton.disabled = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
size.addEventListener("change", () => {
|
|
106
|
+
if (pair) {
|
|
107
|
+
pair = null;
|
|
108
|
+
publicOut.write("");
|
|
109
|
+
privateOut.write("");
|
|
110
|
+
summary.textContent = "";
|
|
111
|
+
sync();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
showPrivate.input.addEventListener("change", sync);
|
|
115
|
+
|
|
116
|
+
sync();
|
|
117
|
+
|
|
118
|
+
const shell = toolcard({
|
|
119
|
+
body: bench(
|
|
120
|
+
stack(
|
|
121
|
+
panel("Key size", size, "2048 is the common floor. 4096 is slower and rarely needed."),
|
|
122
|
+
showPrivate,
|
|
123
|
+
error,
|
|
124
|
+
el("p", { class: "m-0 text-2xs leading-[1.6] text-ink-3" },
|
|
125
|
+
"Generated with the browser's RSA implementation. The keys exist only in this page; closing it discards them. For production, generate keys on a machine you control.")
|
|
126
|
+
),
|
|
127
|
+
stack(summary, empty, publicBox, privateBox)
|
|
128
|
+
),
|
|
129
|
+
foot: actions(downloadPublic, downloadPrivate, copyPublic, copyPrivate, generateButton),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
slot.append(shell.node);
|
|
133
|
+
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default { mount };
|