jwk-cli-tool 1.0.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 ADDED
@@ -0,0 +1,76 @@
1
+ # JWK CLI Tool
2
+
3
+ Simple interactive CLI to generate:
4
+ - PEM key pairs (`.private.pem`, `.public.pem`)
5
+ - JWK JSON files (`.private.jwk.json`, `.public.jwk.json`)
6
+
7
+ ## Requirements
8
+
9
+ - Node.js 18 or newer
10
+ - npm
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install
16
+ ```
17
+
18
+ ## Run
19
+
20
+ ### Development mode
21
+
22
+ ```bash
23
+ npm run dev
24
+ ```
25
+
26
+ ### Production mode
27
+
28
+ ```bash
29
+ npm run build
30
+ npm start
31
+ ```
32
+
33
+ ## How to use
34
+
35
+ When the CLI starts, choose one option:
36
+
37
+ 1. `Generate new PEM key pair`
38
+ 2. `Generate JWK JSON files`
39
+ 3. `Exit`
40
+
41
+ ### 1) Generate new PEM key pair
42
+
43
+ You will be asked:
44
+ - key name
45
+ - key type (`EC` or `RSA`)
46
+ - algorithm (based on selected type)
47
+
48
+ Files created in `keys/`:
49
+ - `keys/<name>.private.pem`
50
+ - `keys/<name>.public.pem`
51
+
52
+ ### 2) Generate JWK JSON files
53
+
54
+ You will be asked:
55
+ - PEM source:
56
+ - generate new PEM key pair, or
57
+ - use existing PEM files from `keys/`
58
+ - `use` (`sig` or `enc`)
59
+ - `kid` (defaults to key name)
60
+ - algorithm only when needed (for existing RSA PEM)
61
+
62
+ Files created in `outputs/`:
63
+ - `outputs/<name>.private.jwk.json`
64
+ - `outputs/<name>.public.jwk.json`
65
+
66
+ The CLI also prints a public JWK preview in the terminal.
67
+
68
+ ## Supported algorithms
69
+
70
+ - EC: `ES256`, `ES384`, `ES512`
71
+ - RSA: `RS256`, `RS384`, `RS512`
72
+
73
+ ## Notes
74
+
75
+ - `keys/` and `outputs/` are auto-created if missing.
76
+ - If a key name already exists, CLI asks whether to overwrite.
package/dist/app.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatErrorMessage = formatErrorMessage;
4
+ exports.runCliApp = runCliApp;
5
+ const prompts_1 = require("@inquirer/prompts");
6
+ const fileUtils_1 = require("./functions/fileUtils");
7
+ const generateKey_1 = require("./prompts/generateKey");
8
+ const generateJwk_1 = require("./prompts/generateJwk");
9
+ function formatErrorMessage(error) {
10
+ if (error instanceof Error) {
11
+ return error.message;
12
+ }
13
+ return String(error);
14
+ }
15
+ async function promptMainMenu() {
16
+ const action = await (0, prompts_1.select)({
17
+ message: "What would you like to do?",
18
+ choices: [
19
+ { name: "1. Generate new PEM key pair", value: "generate-key" },
20
+ { name: "2. Generate JWK JSON files", value: "generate-jwk" },
21
+ { name: "3. Exit", value: "exit" }
22
+ ]
23
+ });
24
+ return action;
25
+ }
26
+ async function runCliApp() {
27
+ await (0, fileUtils_1.ensureProjectDirs)();
28
+ while (true) {
29
+ try {
30
+ const action = await promptMainMenu();
31
+ if (action === "exit") {
32
+ console.log("Goodbye.");
33
+ return;
34
+ }
35
+ if (action === "generate-key") {
36
+ await (0, generateKey_1.runGenerateKeyFlow)();
37
+ continue;
38
+ }
39
+ await (0, generateJwk_1.runGenerateJwkFlow)();
40
+ }
41
+ catch (error) {
42
+ console.error(`\nOperation failed: ${formatErrorMessage(error)}\n`);
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ensureDir = ensureDir;
7
+ exports.configureStorageRoot = configureStorageRoot;
8
+ exports.getStorageConfig = getStorageConfig;
9
+ exports.ensureProjectDirs = ensureProjectDirs;
10
+ exports.fileExists = fileExists;
11
+ exports.listPemFiles = listPemFiles;
12
+ exports.groupPemPairs = groupPemPairs;
13
+ exports.keyFilePath = keyFilePath;
14
+ exports.jwkFilePath = jwkFilePath;
15
+ exports.writeJson = writeJson;
16
+ exports.writePem = writePem;
17
+ exports.readTextFile = readTextFile;
18
+ exports.relativeToRoot = relativeToRoot;
19
+ exports.resolveInKeysDir = resolveInKeysDir;
20
+ const promises_1 = require("fs/promises");
21
+ const path_1 = __importDefault(require("path"));
22
+ function buildStorageConfig(rootDir) {
23
+ const resolvedRoot = path_1.default.resolve(rootDir);
24
+ return {
25
+ rootDir: resolvedRoot,
26
+ keysDir: path_1.default.join(resolvedRoot, "keys"),
27
+ outputsDir: path_1.default.join(resolvedRoot, "outputs")
28
+ };
29
+ }
30
+ let storageConfig = buildStorageConfig(process.cwd());
31
+ async function ensureDir(dirPath) {
32
+ await (0, promises_1.mkdir)(dirPath, { recursive: true });
33
+ }
34
+ function configureStorageRoot(rootDir) {
35
+ storageConfig = buildStorageConfig(rootDir);
36
+ return storageConfig;
37
+ }
38
+ function getStorageConfig() {
39
+ return storageConfig;
40
+ }
41
+ async function ensureProjectDirs() {
42
+ await Promise.all([ensureDir(storageConfig.keysDir), ensureDir(storageConfig.outputsDir)]);
43
+ }
44
+ async function fileExists(filePath) {
45
+ try {
46
+ await (0, promises_1.access)(filePath);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ async function listPemFiles() {
54
+ await ensureDir(storageConfig.keysDir);
55
+ const entries = await (0, promises_1.readdir)(storageConfig.keysDir, { withFileTypes: true });
56
+ return entries
57
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".pem"))
58
+ .map((entry) => entry.name)
59
+ .sort((a, b) => a.localeCompare(b));
60
+ }
61
+ function groupPemPairs(files) {
62
+ const byName = new Map();
63
+ const otherPem = [];
64
+ for (const file of files) {
65
+ if (file.endsWith(".private.pem")) {
66
+ const name = file.slice(0, -".private.pem".length);
67
+ const current = byName.get(name) ?? {};
68
+ current.privateFile = file;
69
+ byName.set(name, current);
70
+ continue;
71
+ }
72
+ if (file.endsWith(".public.pem")) {
73
+ const name = file.slice(0, -".public.pem".length);
74
+ const current = byName.get(name) ?? {};
75
+ current.publicFile = file;
76
+ byName.set(name, current);
77
+ continue;
78
+ }
79
+ otherPem.push(file);
80
+ }
81
+ const pairs = [];
82
+ const privateOnly = [];
83
+ const publicOnly = [];
84
+ for (const [name, pair] of byName.entries()) {
85
+ if (pair.privateFile && pair.publicFile) {
86
+ pairs.push({ name, privateFile: pair.privateFile, publicFile: pair.publicFile });
87
+ continue;
88
+ }
89
+ if (pair.privateFile) {
90
+ privateOnly.push(pair.privateFile);
91
+ }
92
+ if (pair.publicFile) {
93
+ publicOnly.push(pair.publicFile);
94
+ }
95
+ }
96
+ pairs.sort((a, b) => a.name.localeCompare(b.name));
97
+ privateOnly.sort((a, b) => a.localeCompare(b));
98
+ publicOnly.sort((a, b) => a.localeCompare(b));
99
+ otherPem.sort((a, b) => a.localeCompare(b));
100
+ return { pairs, privateOnly, publicOnly, otherPem };
101
+ }
102
+ function keyFilePath(name, visibility) {
103
+ return path_1.default.join(storageConfig.keysDir, `${name}.${visibility}.pem`);
104
+ }
105
+ function jwkFilePath(name, visibility) {
106
+ return path_1.default.join(storageConfig.outputsDir, `${name}.${visibility}.jwk.json`);
107
+ }
108
+ async function writeJson(filePath, obj) {
109
+ await (0, promises_1.writeFile)(filePath, `${JSON.stringify(obj, null, 2)}\n`, "utf8");
110
+ }
111
+ async function writePem(filePath, pem) {
112
+ await (0, promises_1.writeFile)(filePath, pem, "utf8");
113
+ }
114
+ async function readTextFile(filePath) {
115
+ return (0, promises_1.readFile)(filePath, "utf8");
116
+ }
117
+ function relativeToRoot(filePath) {
118
+ return path_1.default.relative(storageConfig.rootDir, filePath);
119
+ }
120
+ function resolveInKeysDir(fileName) {
121
+ return path_1.default.join(storageConfig.keysDir, fileName);
122
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RSA_ALGORITHMS = exports.EC_ALGORITHMS = exports.ALL_ALGORITHMS = exports.DEFAULT_ALGORITHM = void 0;
4
+ exports.isEcAlgorithm = isEcAlgorithm;
5
+ exports.generatePemKeyPair = generatePemKeyPair;
6
+ const crypto_1 = require("crypto");
7
+ exports.DEFAULT_ALGORITHM = "ES256";
8
+ exports.ALL_ALGORITHMS = ["ES256", "ES384", "ES512", "RS256", "RS384", "RS512"];
9
+ exports.EC_ALGORITHMS = ["ES256", "ES384", "ES512"];
10
+ exports.RSA_ALGORITHMS = ["RS256", "RS384", "RS512"];
11
+ const EC_CURVE_BY_ALGORITHM = {
12
+ ES256: "P-256",
13
+ ES384: "P-384",
14
+ ES512: "P-521"
15
+ };
16
+ const RSA_SIZE_BY_ALGORITHM = {
17
+ RS256: 2048,
18
+ RS384: 3072,
19
+ RS512: 4096
20
+ };
21
+ function isEcAlgorithm(algorithm) {
22
+ return algorithm.startsWith("ES");
23
+ }
24
+ function generatePemKeyPair(algorithm) {
25
+ if (isEcAlgorithm(algorithm)) {
26
+ const { privateKey, publicKey } = (0, crypto_1.generateKeyPairSync)("ec", {
27
+ namedCurve: EC_CURVE_BY_ALGORITHM[algorithm]
28
+ });
29
+ return {
30
+ privatePem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
31
+ publicPem: publicKey.export({ type: "spki", format: "pem" }).toString()
32
+ };
33
+ }
34
+ const { privateKey, publicKey } = (0, crypto_1.generateKeyPairSync)("rsa", {
35
+ modulusLength: RSA_SIZE_BY_ALGORITHM[algorithm]
36
+ });
37
+ return {
38
+ privatePem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
39
+ publicPem: publicKey.export({ type: "spki", format: "pem" }).toString()
40
+ };
41
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectAlgorithmFromPem = detectAlgorithmFromPem;
4
+ exports.convertPemPairToJwks = convertPemPairToJwks;
5
+ const crypto_1 = require("crypto");
6
+ const EC_ALGORITHM_BY_CURVE = {
7
+ "P-256": "ES256",
8
+ "P-384": "ES384",
9
+ "P-521": "ES512"
10
+ };
11
+ function exportJwk(keyObject) {
12
+ return keyObject.export({ format: "jwk" });
13
+ }
14
+ function withMetadata(jwk, algorithm, kid, use) {
15
+ return {
16
+ ...jwk,
17
+ use,
18
+ alg: algorithm,
19
+ kid
20
+ };
21
+ }
22
+ function detectAlgorithmFromPem(publicPem) {
23
+ const publicKey = (0, crypto_1.createPublicKey)(publicPem);
24
+ const jwk = exportJwk(publicKey);
25
+ if (jwk.kty === "EC" && typeof jwk.crv === "string") {
26
+ return EC_ALGORITHM_BY_CURVE[jwk.crv];
27
+ }
28
+ return undefined;
29
+ }
30
+ function convertPemPairToJwks(privatePem, publicPem, algorithm, kid, use = "sig") {
31
+ const privateKey = (0, crypto_1.createPrivateKey)(privatePem);
32
+ const publicKey = (0, crypto_1.createPublicKey)(publicPem);
33
+ const privateJwk = withMetadata(exportJwk(privateKey), algorithm, kid, use);
34
+ const publicJwk = withMetadata(exportJwk(publicKey), algorithm, kid, use);
35
+ // Public JWK should never expose private parameters even if a malformed key slips in.
36
+ for (const privateField of ["d", "p", "q", "dp", "dq", "qi", "oth"]) {
37
+ delete publicJwk[privateField];
38
+ }
39
+ return { privateJwk, publicJwk };
40
+ }
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const app_1 = require("./app");
4
+ const fileUtils_1 = require("./functions/fileUtils");
5
+ (0, fileUtils_1.configureStorageRoot)(process.cwd());
6
+ void (0, app_1.runCliApp)().catch((error) => {
7
+ console.error(`Fatal error: ${(0, app_1.formatErrorMessage)(error)}`);
8
+ process.exit(1);
9
+ });
package/dist/npx.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const prompts_1 = require("@inquirer/prompts");
8
+ const path_1 = __importDefault(require("path"));
9
+ const app_1 = require("./app");
10
+ const fileUtils_1 = require("./functions/fileUtils");
11
+ async function promptForBaseFolder() {
12
+ const defaultBaseFolder = path_1.default.join(process.cwd(), "jwk");
13
+ const raw = await (0, prompts_1.input)({
14
+ message: "Base folder for generated files (keys/outputs will be created inside):",
15
+ default: defaultBaseFolder,
16
+ validate: (value) => (value.trim() ? true : "Base folder is required.")
17
+ });
18
+ return path_1.default.resolve(raw.trim() || defaultBaseFolder);
19
+ }
20
+ async function main() {
21
+ const baseFolder = await promptForBaseFolder();
22
+ const config = (0, fileUtils_1.configureStorageRoot)(baseFolder);
23
+ console.log(`\nSaving keys to : ${config.keysDir}`);
24
+ console.log(`Saving JWK files : ${config.outputsDir}\n`);
25
+ await (0, app_1.runCliApp)();
26
+ }
27
+ void main().catch((error) => {
28
+ console.error(`Fatal error: ${(0, app_1.formatErrorMessage)(error)}`);
29
+ process.exit(1);
30
+ });
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runGenerateJwkFlow = runGenerateJwkFlow;
4
+ const prompts_1 = require("@inquirer/prompts");
5
+ const fileUtils_1 = require("../functions/fileUtils");
6
+ const keyGen_1 = require("../functions/keyGen");
7
+ const pemToJwk_1 = require("../functions/pemToJwk");
8
+ const generateKey_1 = require("./generateKey");
9
+ function deriveNameFromPemFile(fileName) {
10
+ if (fileName.endsWith(".private.pem")) {
11
+ return fileName.slice(0, -".private.pem".length);
12
+ }
13
+ if (fileName.endsWith(".public.pem")) {
14
+ return fileName.slice(0, -".public.pem".length);
15
+ }
16
+ if (fileName.endsWith(".pem")) {
17
+ return fileName.slice(0, -".pem".length);
18
+ }
19
+ return fileName;
20
+ }
21
+ async function selectExistingPemFiles() {
22
+ const files = await (0, fileUtils_1.listPemFiles)();
23
+ if (files.length === 0) {
24
+ return null;
25
+ }
26
+ const grouped = (0, fileUtils_1.groupPemPairs)(files);
27
+ if (grouped.pairs.length > 0) {
28
+ const selectedName = await (0, prompts_1.select)({
29
+ message: "Select key pair from /keys:",
30
+ choices: grouped.pairs.map((pair) => ({
31
+ name: pair.name,
32
+ value: pair.name
33
+ }))
34
+ });
35
+ const selectedPair = grouped.pairs.find((pair) => pair.name === selectedName);
36
+ if (!selectedPair) {
37
+ throw new Error("Selected pair could not be resolved.");
38
+ }
39
+ return {
40
+ name: selectedPair.name,
41
+ privatePath: (0, fileUtils_1.resolveInKeysDir)(selectedPair.privateFile),
42
+ publicPath: (0, fileUtils_1.resolveInKeysDir)(selectedPair.publicFile)
43
+ };
44
+ }
45
+ const privateCandidates = [...grouped.privateOnly, ...grouped.otherPem];
46
+ const publicCandidates = [...grouped.publicOnly, ...grouped.otherPem];
47
+ if (privateCandidates.length === 0 || publicCandidates.length === 0) {
48
+ throw new Error("Could not find both private and public PEM files.");
49
+ }
50
+ const privateFile = await (0, prompts_1.select)({
51
+ message: "Select private PEM file:",
52
+ choices: privateCandidates.map((file) => ({ name: file, value: file }))
53
+ });
54
+ const publicFile = await (0, prompts_1.select)({
55
+ message: "Select public PEM file:",
56
+ choices: publicCandidates
57
+ .filter((file) => file !== privateFile)
58
+ .map((file) => ({ name: file, value: file }))
59
+ });
60
+ const privateName = deriveNameFromPemFile(privateFile);
61
+ const publicName = deriveNameFromPemFile(publicFile);
62
+ const defaultName = privateName || publicName || "key";
63
+ const nameInput = await (0, prompts_1.input)({
64
+ message: "Output file name prefix:",
65
+ default: privateName === publicName ? defaultName : `${privateName}-${publicName}`,
66
+ validate: (value) => (value.trim() ? true : "Name is required.")
67
+ });
68
+ return {
69
+ name: nameInput.trim(),
70
+ privatePath: (0, fileUtils_1.resolveInKeysDir)(privateFile),
71
+ publicPath: (0, fileUtils_1.resolveInKeysDir)(publicFile)
72
+ };
73
+ }
74
+ async function runGenerateJwkFlow() {
75
+ const source = await (0, prompts_1.select)({
76
+ message: "How do you want to provide the PEM keys?",
77
+ choices: [
78
+ {
79
+ name: "Generate new PEM key pair (then use it)",
80
+ value: "generate-new"
81
+ },
82
+ {
83
+ name: "Use existing PEM file from /keys folder",
84
+ value: "use-existing"
85
+ }
86
+ ]
87
+ });
88
+ let name;
89
+ let privatePem;
90
+ let publicPem;
91
+ let algorithm;
92
+ if (source === "generate-new") {
93
+ const generated = await (0, generateKey_1.runGenerateKeyFlow)();
94
+ name = generated.name;
95
+ algorithm = generated.algorithm;
96
+ [privatePem, publicPem] = await Promise.all([
97
+ (0, fileUtils_1.readTextFile)(generated.privatePath),
98
+ (0, fileUtils_1.readTextFile)(generated.publicPath)
99
+ ]);
100
+ }
101
+ else {
102
+ const selected = await selectExistingPemFiles();
103
+ if (!selected) {
104
+ console.log('\nNo .pem files found in /keys folder. Please generate one first.\n');
105
+ return;
106
+ }
107
+ name = selected.name;
108
+ [privatePem, publicPem] = await Promise.all([
109
+ (0, fileUtils_1.readTextFile)(selected.privatePath),
110
+ (0, fileUtils_1.readTextFile)(selected.publicPath)
111
+ ]);
112
+ algorithm = (0, pemToJwk_1.detectAlgorithmFromPem)(publicPem);
113
+ if (!algorithm) {
114
+ algorithm = await (0, generateKey_1.promptForAlgorithm)(keyGen_1.RSA_ALGORITHMS, "RS256");
115
+ }
116
+ }
117
+ const use = await (0, prompts_1.select)({
118
+ message: "Key use (use):",
119
+ choices: [
120
+ { name: "Signature (sig)", value: "sig" },
121
+ { name: "Encryption (enc)", value: "enc" }
122
+ ],
123
+ default: "sig"
124
+ });
125
+ const kidInput = await (0, prompts_1.input)({
126
+ message: "kid (leave blank to use key name):",
127
+ default: name
128
+ });
129
+ const kid = kidInput.trim() || name;
130
+ const { privateJwk, publicJwk } = (0, pemToJwk_1.convertPemPairToJwks)(privatePem, publicPem, algorithm, kid, use);
131
+ const privateOutputPath = (0, fileUtils_1.jwkFilePath)(name, "private");
132
+ const publicOutputPath = (0, fileUtils_1.jwkFilePath)(name, "public");
133
+ await Promise.all([
134
+ (0, fileUtils_1.writeJson)(privateOutputPath, privateJwk),
135
+ (0, fileUtils_1.writeJson)(publicOutputPath, publicJwk)
136
+ ]);
137
+ console.log("\nPublic JWK preview:");
138
+ console.log(JSON.stringify(publicJwk, null, 2));
139
+ console.log("\nGenerated JWK files.");
140
+ console.log(`Private JWK: ${(0, fileUtils_1.relativeToRoot)(privateOutputPath)}`);
141
+ console.log(`Public JWK : ${(0, fileUtils_1.relativeToRoot)(publicOutputPath)}\n`);
142
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promptForAlgorithm = promptForAlgorithm;
4
+ exports.promptForKeyType = promptForKeyType;
5
+ exports.runGenerateKeyFlow = runGenerateKeyFlow;
6
+ const prompts_1 = require("@inquirer/prompts");
7
+ const keyGen_1 = require("../functions/keyGen");
8
+ const fileUtils_1 = require("../functions/fileUtils");
9
+ const ALGORITHM_LABELS = {
10
+ ES256: "ES256 (EC P-256)",
11
+ ES384: "ES384 (EC P-384)",
12
+ ES512: "ES512 (EC P-521)",
13
+ RS256: "RS256 (RSA 2048-bit)",
14
+ RS384: "RS384 (RSA 3072-bit)",
15
+ RS512: "RS512 (RSA 4096-bit)"
16
+ };
17
+ const KEY_TYPE_LABELS = {
18
+ EC: "EC (Elliptic Curve)",
19
+ RSA: "RSA"
20
+ };
21
+ function normalizeName(value) {
22
+ return value.trim();
23
+ }
24
+ async function promptForKeyName() {
25
+ while (true) {
26
+ const rawName = await (0, prompts_1.input)({
27
+ message: "Key name (used as filename prefix):",
28
+ validate: (value) => {
29
+ if (!value.trim()) {
30
+ return "Key name is required.";
31
+ }
32
+ if (!/^[A-Za-z0-9._-]+$/.test(value.trim())) {
33
+ return "Use only letters, numbers, dot, underscore, or hyphen.";
34
+ }
35
+ return true;
36
+ }
37
+ });
38
+ const name = normalizeName(rawName);
39
+ const privatePath = (0, fileUtils_1.keyFilePath)(name, "private");
40
+ const publicPath = (0, fileUtils_1.keyFilePath)(name, "public");
41
+ const [privateExists, publicExists] = await Promise.all([
42
+ (0, fileUtils_1.fileExists)(privatePath),
43
+ (0, fileUtils_1.fileExists)(publicPath)
44
+ ]);
45
+ if (!privateExists && !publicExists) {
46
+ return name;
47
+ }
48
+ const overwrite = await (0, prompts_1.confirm)({
49
+ message: `Key files for "${name}" already exist. Overwrite them?`,
50
+ default: false
51
+ });
52
+ if (overwrite) {
53
+ return name;
54
+ }
55
+ }
56
+ }
57
+ async function promptForAlgorithm(allowed = keyGen_1.ALL_ALGORITHMS, defaultValue = keyGen_1.DEFAULT_ALGORITHM) {
58
+ const selected = await (0, prompts_1.select)({
59
+ message: "Algorithm:",
60
+ choices: allowed.map((algorithm) => ({
61
+ name: ALGORITHM_LABELS[algorithm],
62
+ value: algorithm
63
+ })),
64
+ default: defaultValue
65
+ });
66
+ return selected;
67
+ }
68
+ async function promptForKeyType(defaultValue = "EC") {
69
+ const selected = await (0, prompts_1.select)({
70
+ message: "Key type (kty):",
71
+ choices: ["EC", "RSA"].map((keyType) => ({
72
+ name: KEY_TYPE_LABELS[keyType],
73
+ value: keyType
74
+ })),
75
+ default: defaultValue
76
+ });
77
+ return selected;
78
+ }
79
+ async function runGenerateKeyFlow() {
80
+ const name = await promptForKeyName();
81
+ const keyType = await promptForKeyType();
82
+ const algorithm = keyType === "EC"
83
+ ? await promptForAlgorithm(keyGen_1.EC_ALGORITHMS, "ES256")
84
+ : await promptForAlgorithm(keyGen_1.RSA_ALGORITHMS, "RS256");
85
+ const { privatePem, publicPem } = (0, keyGen_1.generatePemKeyPair)(algorithm);
86
+ const privatePath = (0, fileUtils_1.keyFilePath)(name, "private");
87
+ const publicPath = (0, fileUtils_1.keyFilePath)(name, "public");
88
+ await Promise.all([
89
+ (0, fileUtils_1.writePem)(privatePath, privatePem),
90
+ (0, fileUtils_1.writePem)(publicPath, publicPem)
91
+ ]);
92
+ console.log(`\nGenerated PEM key pair for "${name}".`);
93
+ console.log(`Private key: ${(0, fileUtils_1.relativeToRoot)(privatePath)}`);
94
+ console.log(`Public key : ${(0, fileUtils_1.relativeToRoot)(publicPath)}\n`);
95
+ return {
96
+ name,
97
+ algorithm,
98
+ privatePath,
99
+ publicPath
100
+ };
101
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "jwk-cli-tool",
3
+ "version": "1.0.0",
4
+ "description": "Interactive CLI for generating PEM and JWK files",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "jwk-cli-tool": "dist/npx.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "scripts": {
16
+ "dev": "ts-node src/index.ts",
17
+ "build": "tsc",
18
+ "start": "node dist/index.js",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "cli",
25
+ "jwk",
26
+ "pem",
27
+ "crypto",
28
+ "typescript"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@inquirer/prompts": "^7.3.2"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.17.57",
36
+ "ts-node": "^10.9.2",
37
+ "typescript": "^5.8.3",
38
+ "vitest": "^2.1.9"
39
+ }
40
+ }