lockly 0.1.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/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/cli.js +114 -0
- package/dist/index.cjs +68 -0
- package/dist/index.d.cts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +66 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lockly contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# lockly
|
|
2
|
+
|
|
3
|
+
Cryptographically secure password generator CLI.
|
|
4
|
+
|
|
5
|
+
Generates unpredictable random passwords instantly using `node:crypto.getRandomValues()`.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Cryptographically secure** — CSPRNG via `node:crypto`
|
|
10
|
+
- **Fast** — under 50ms response time
|
|
11
|
+
- **Customizable** — length, character sets, count
|
|
12
|
+
- **Pipe-friendly** — clean stdout output, no ANSI colors
|
|
13
|
+
- **Zero install** — run instantly with `npx`
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Run instantly with npx (recommended)
|
|
19
|
+
npx lockly
|
|
20
|
+
|
|
21
|
+
# Or install globally
|
|
22
|
+
npm install -g lockly
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Basic
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Default: generate a 16-character password
|
|
31
|
+
lockly
|
|
32
|
+
|
|
33
|
+
# Set length (32 characters)
|
|
34
|
+
lockly -l 32
|
|
35
|
+
lockly --length 32
|
|
36
|
+
|
|
37
|
+
# Generate multiple passwords (5)
|
|
38
|
+
lockly -c 5
|
|
39
|
+
lockly --count 5
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Character set filtering
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Exclude symbols
|
|
46
|
+
lockly --no-symbols
|
|
47
|
+
|
|
48
|
+
# Digits only (PIN)
|
|
49
|
+
lockly --no-uppercase --no-lowercase --no-symbols -l 6
|
|
50
|
+
|
|
51
|
+
# Uppercase and digits only
|
|
52
|
+
lockly --no-lowercase --no-symbols
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Piping
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Copy to clipboard (macOS)
|
|
59
|
+
lockly | pbcopy
|
|
60
|
+
|
|
61
|
+
# Copy to clipboard (Linux)
|
|
62
|
+
lockly | xclip -selection clipboard
|
|
63
|
+
|
|
64
|
+
# Copy to clipboard (Windows PowerShell)
|
|
65
|
+
lockly | Set-Clipboard
|
|
66
|
+
|
|
67
|
+
# Save to file
|
|
68
|
+
lockly -c 10 -l 32 > passwords.txt
|
|
69
|
+
|
|
70
|
+
# Set as environment variable
|
|
71
|
+
export DB_PASSWORD=$(lockly -l 24 --no-symbols)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Options
|
|
75
|
+
|
|
76
|
+
| Option | Description | Default | Range |
|
|
77
|
+
|--------|-------------|---------|-------|
|
|
78
|
+
| `-l, --length <number>` | Password length | 16 | 1–1024 |
|
|
79
|
+
| `-c, --count <number>` | Number of passwords | 1 | 1+ |
|
|
80
|
+
| `--no-uppercase` | Exclude uppercase (A-Z) | included | - |
|
|
81
|
+
| `--no-lowercase` | Exclude lowercase (a-z) | included | - |
|
|
82
|
+
| `--no-numbers` | Exclude digits (0-9) | included | - |
|
|
83
|
+
| `--no-symbols` | Exclude symbols | included | - |
|
|
84
|
+
| `-V, --version` | Show version | - | - |
|
|
85
|
+
| `-h, --help` | Show help | - | - |
|
|
86
|
+
|
|
87
|
+
## Security
|
|
88
|
+
|
|
89
|
+
- **CSPRNG**: Uses `node:crypto.getRandomValues()` instead of `Math.random()`
|
|
90
|
+
- **Local execution**: No network requests — passwords are generated locally
|
|
91
|
+
- **Stateless**: Generated passwords are never stored
|
|
92
|
+
- **No modulo bias**: Rejection sampling ensures uniform distribution
|
|
93
|
+
|
|
94
|
+
### Security tips
|
|
95
|
+
|
|
96
|
+
- Store generated passwords immediately in a secure location (e.g. password manager)
|
|
97
|
+
- Use piping to avoid passwords appearing in terminal history
|
|
98
|
+
- Use `-l 32` or longer for high-security purposes (root accounts, financial services, etc.)
|
|
99
|
+
|
|
100
|
+
## Programmatic API
|
|
101
|
+
|
|
102
|
+
Use lockly as a library in your TypeScript/JavaScript project.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { generatePassword } from 'lockly';
|
|
106
|
+
|
|
107
|
+
// Default usage
|
|
108
|
+
const passwords = generatePassword();
|
|
109
|
+
console.log(passwords[0]); // 16-character password
|
|
110
|
+
|
|
111
|
+
// With options
|
|
112
|
+
const customPasswords = generatePassword({
|
|
113
|
+
length: 32,
|
|
114
|
+
count: 5,
|
|
115
|
+
uppercase: true,
|
|
116
|
+
lowercase: true,
|
|
117
|
+
numbers: true,
|
|
118
|
+
symbols: false
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
- Node.js 20+
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import { getRandomValues } from 'crypto';
|
|
7
|
+
|
|
8
|
+
var UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
9
|
+
var LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
|
10
|
+
var NUMBERS = "0123456789";
|
|
11
|
+
var SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
|
12
|
+
function buildPool(options2) {
|
|
13
|
+
let pool = "";
|
|
14
|
+
if (options2.uppercase) pool += UPPERCASE;
|
|
15
|
+
if (options2.lowercase) pool += LOWERCASE;
|
|
16
|
+
if (options2.numbers) pool += NUMBERS;
|
|
17
|
+
if (options2.symbols) pool += SYMBOLS;
|
|
18
|
+
return pool;
|
|
19
|
+
}
|
|
20
|
+
function generateSingle(length2, pool) {
|
|
21
|
+
const poolLength = pool.length;
|
|
22
|
+
const limit = 256 - 256 % poolLength;
|
|
23
|
+
const chars = [];
|
|
24
|
+
while (chars.length < length2) {
|
|
25
|
+
const remaining = length2 - chars.length;
|
|
26
|
+
const bufferSize = remaining * 2;
|
|
27
|
+
const randomBytes = new Uint8Array(bufferSize);
|
|
28
|
+
getRandomValues(randomBytes);
|
|
29
|
+
for (let i = 0; i < randomBytes.length && chars.length < length2; i++) {
|
|
30
|
+
const byte = randomBytes[i];
|
|
31
|
+
if (byte < limit) {
|
|
32
|
+
chars.push(pool[byte % poolLength]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return chars.join("");
|
|
37
|
+
}
|
|
38
|
+
function validateOptions(options2) {
|
|
39
|
+
if (!Number.isInteger(options2.length) || options2.length < 1) {
|
|
40
|
+
throw new Error("\uAE38\uC774\uB294 1~1024 \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
41
|
+
}
|
|
42
|
+
if (options2.length > 1024) {
|
|
43
|
+
throw new Error("\uAE38\uC774\uB294 1~1024 \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isInteger(options2.count) || options2.count < 1) {
|
|
46
|
+
throw new Error("count\uB294 1 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
47
|
+
}
|
|
48
|
+
if (!options2.uppercase && !options2.lowercase && !options2.numbers && !options2.symbols) {
|
|
49
|
+
throw new Error("\uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uBB38\uC790\uC14B\uC744 \uD3EC\uD568\uD574\uC57C \uD569\uB2C8\uB2E4");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function generatePassword(options2) {
|
|
53
|
+
const resolved = {
|
|
54
|
+
length: options2?.length ?? 16,
|
|
55
|
+
count: options2?.count ?? 1,
|
|
56
|
+
uppercase: options2?.uppercase ?? true,
|
|
57
|
+
lowercase: options2?.lowercase ?? true,
|
|
58
|
+
numbers: options2?.numbers ?? true,
|
|
59
|
+
symbols: options2?.symbols ?? true
|
|
60
|
+
};
|
|
61
|
+
validateOptions(resolved);
|
|
62
|
+
const pool = buildPool(resolved);
|
|
63
|
+
const passwords = [];
|
|
64
|
+
for (let i = 0; i < resolved.count; i++) {
|
|
65
|
+
passwords.push(generateSingle(resolved.length, pool));
|
|
66
|
+
}
|
|
67
|
+
return passwords;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/cli.ts
|
|
71
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
72
|
+
var __dirname2 = dirname(__filename2);
|
|
73
|
+
var packageJsonPath = join(__dirname2, "../package.json");
|
|
74
|
+
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
75
|
+
var version = packageJson.version;
|
|
76
|
+
var program = new Command();
|
|
77
|
+
program.name("lockly").description("Cryptographically secure random password generator").version(version, "-V, --version", "\uBC84\uC804 \uD45C\uC2DC").option(
|
|
78
|
+
"-l, --length <number>",
|
|
79
|
+
"\uD328\uC2A4\uC6CC\uB4DC \uAE38\uC774 (\uAE30\uBCF8: 16, \uBC94\uC704: 1-1024)",
|
|
80
|
+
"16"
|
|
81
|
+
).option("-c, --count <number>", "\uC0DD\uC131 \uAC1C\uC218 (\uAE30\uBCF8: 1)", "1").option("--no-uppercase", "\uB300\uBB38\uC790(A-Z) \uC81C\uC678").option("--no-lowercase", "\uC18C\uBB38\uC790(a-z) \uC81C\uC678").option("--no-numbers", "\uC22B\uC790(0-9) \uC81C\uC678").option("--no-symbols", "\uD2B9\uC218\uBB38\uC790 \uC81C\uC678").helpOption("-h, --help", "\uB3C4\uC6C0\uB9D0");
|
|
82
|
+
program.parse();
|
|
83
|
+
var options = program.opts();
|
|
84
|
+
var length = parseInt(options.length, 10);
|
|
85
|
+
var count = parseInt(options.count, 10);
|
|
86
|
+
if (isNaN(length)) {
|
|
87
|
+
console.error("\uC720\uD6A8\uD55C \uC22B\uC790\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694 (length)");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (isNaN(count)) {
|
|
91
|
+
console.error("\uC720\uD6A8\uD55C \uC22B\uC790\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694 (count)");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const passwords = generatePassword({
|
|
96
|
+
length,
|
|
97
|
+
count,
|
|
98
|
+
uppercase: options.uppercase,
|
|
99
|
+
lowercase: options.lowercase,
|
|
100
|
+
numbers: options.numbers,
|
|
101
|
+
symbols: options.symbols
|
|
102
|
+
});
|
|
103
|
+
for (const password of passwords) {
|
|
104
|
+
console.log(password);
|
|
105
|
+
}
|
|
106
|
+
process.exit(0);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error instanceof Error) {
|
|
109
|
+
console.error(error.message);
|
|
110
|
+
} else {
|
|
111
|
+
console.error("\uC54C \uC218 \uC5C6\uB294 \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4");
|
|
112
|
+
}
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// src/generator.ts
|
|
6
|
+
var UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
7
|
+
var LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
|
8
|
+
var NUMBERS = "0123456789";
|
|
9
|
+
var SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
|
10
|
+
function buildPool(options) {
|
|
11
|
+
let pool = "";
|
|
12
|
+
if (options.uppercase) pool += UPPERCASE;
|
|
13
|
+
if (options.lowercase) pool += LOWERCASE;
|
|
14
|
+
if (options.numbers) pool += NUMBERS;
|
|
15
|
+
if (options.symbols) pool += SYMBOLS;
|
|
16
|
+
return pool;
|
|
17
|
+
}
|
|
18
|
+
function generateSingle(length, pool) {
|
|
19
|
+
const poolLength = pool.length;
|
|
20
|
+
const limit = 256 - 256 % poolLength;
|
|
21
|
+
const chars = [];
|
|
22
|
+
while (chars.length < length) {
|
|
23
|
+
const remaining = length - chars.length;
|
|
24
|
+
const bufferSize = remaining * 2;
|
|
25
|
+
const randomBytes = new Uint8Array(bufferSize);
|
|
26
|
+
crypto.getRandomValues(randomBytes);
|
|
27
|
+
for (let i = 0; i < randomBytes.length && chars.length < length; i++) {
|
|
28
|
+
const byte = randomBytes[i];
|
|
29
|
+
if (byte < limit) {
|
|
30
|
+
chars.push(pool[byte % poolLength]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return chars.join("");
|
|
35
|
+
}
|
|
36
|
+
function validateOptions(options) {
|
|
37
|
+
if (!Number.isInteger(options.length) || options.length < 1) {
|
|
38
|
+
throw new Error("\uAE38\uC774\uB294 1~1024 \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
39
|
+
}
|
|
40
|
+
if (options.length > 1024) {
|
|
41
|
+
throw new Error("\uAE38\uC774\uB294 1~1024 \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
42
|
+
}
|
|
43
|
+
if (!Number.isInteger(options.count) || options.count < 1) {
|
|
44
|
+
throw new Error("count\uB294 1 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
45
|
+
}
|
|
46
|
+
if (!options.uppercase && !options.lowercase && !options.numbers && !options.symbols) {
|
|
47
|
+
throw new Error("\uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uBB38\uC790\uC14B\uC744 \uD3EC\uD568\uD574\uC57C \uD569\uB2C8\uB2E4");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function generatePassword(options) {
|
|
51
|
+
const resolved = {
|
|
52
|
+
length: options?.length ?? 16,
|
|
53
|
+
count: options?.count ?? 1,
|
|
54
|
+
uppercase: options?.uppercase ?? true,
|
|
55
|
+
lowercase: options?.lowercase ?? true,
|
|
56
|
+
numbers: options?.numbers ?? true,
|
|
57
|
+
symbols: options?.symbols ?? true
|
|
58
|
+
};
|
|
59
|
+
validateOptions(resolved);
|
|
60
|
+
const pool = buildPool(resolved);
|
|
61
|
+
const passwords = [];
|
|
62
|
+
for (let i = 0; i < resolved.count; i++) {
|
|
63
|
+
passwords.push(generateSingle(resolved.length, pool));
|
|
64
|
+
}
|
|
65
|
+
return passwords;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
exports.generatePassword = generatePassword;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password generation core logic
|
|
3
|
+
* Uses node:crypto for cryptographically secure random generation
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Options for password generation
|
|
7
|
+
*/
|
|
8
|
+
interface GenerateOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Password length (1-1024)
|
|
11
|
+
* @default 16
|
|
12
|
+
*/
|
|
13
|
+
length?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Number of passwords to generate
|
|
16
|
+
* @default 1
|
|
17
|
+
*/
|
|
18
|
+
count?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Include uppercase letters (A-Z)
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
uppercase?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Include lowercase letters (a-z)
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
lowercase?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Include numbers (0-9)
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
numbers?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Include special characters
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
symbols?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate one or more cryptographically secure random passwords.
|
|
42
|
+
*
|
|
43
|
+
* Uses `node:crypto.getRandomValues()` as the entropy source (REQ-1, NFR-1).
|
|
44
|
+
* Applies rejection sampling to avoid modulo bias when the character pool
|
|
45
|
+
* length is not a power of two.
|
|
46
|
+
*
|
|
47
|
+
* @param options - Password generation options. All fields are optional and
|
|
48
|
+
* fall back to safe defaults (length=16, count=1, all charsets enabled).
|
|
49
|
+
* @returns An array of generated password strings. The array length equals
|
|
50
|
+
* `options.count` (default 1).
|
|
51
|
+
* @throws {Error} If options validation fails (invalid length, count, or charsets)
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* // Generate one 16-character password with all charsets
|
|
56
|
+
* const [pw] = generatePassword();
|
|
57
|
+
*
|
|
58
|
+
* // Generate three 32-character passwords without symbols
|
|
59
|
+
* const pws = generatePassword({ length: 32, count: 3, symbols: false });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare function generatePassword(options?: GenerateOptions): string[];
|
|
63
|
+
|
|
64
|
+
export { type GenerateOptions, generatePassword };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password generation core logic
|
|
3
|
+
* Uses node:crypto for cryptographically secure random generation
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Options for password generation
|
|
7
|
+
*/
|
|
8
|
+
interface GenerateOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Password length (1-1024)
|
|
11
|
+
* @default 16
|
|
12
|
+
*/
|
|
13
|
+
length?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Number of passwords to generate
|
|
16
|
+
* @default 1
|
|
17
|
+
*/
|
|
18
|
+
count?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Include uppercase letters (A-Z)
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
uppercase?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Include lowercase letters (a-z)
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
lowercase?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Include numbers (0-9)
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
numbers?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Include special characters
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
symbols?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate one or more cryptographically secure random passwords.
|
|
42
|
+
*
|
|
43
|
+
* Uses `node:crypto.getRandomValues()` as the entropy source (REQ-1, NFR-1).
|
|
44
|
+
* Applies rejection sampling to avoid modulo bias when the character pool
|
|
45
|
+
* length is not a power of two.
|
|
46
|
+
*
|
|
47
|
+
* @param options - Password generation options. All fields are optional and
|
|
48
|
+
* fall back to safe defaults (length=16, count=1, all charsets enabled).
|
|
49
|
+
* @returns An array of generated password strings. The array length equals
|
|
50
|
+
* `options.count` (default 1).
|
|
51
|
+
* @throws {Error} If options validation fails (invalid length, count, or charsets)
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* // Generate one 16-character password with all charsets
|
|
56
|
+
* const [pw] = generatePassword();
|
|
57
|
+
*
|
|
58
|
+
* // Generate three 32-character passwords without symbols
|
|
59
|
+
* const pws = generatePassword({ length: 32, count: 3, symbols: false });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare function generatePassword(options?: GenerateOptions): string[];
|
|
63
|
+
|
|
64
|
+
export { type GenerateOptions, generatePassword };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getRandomValues } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/generator.ts
|
|
4
|
+
var UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
5
|
+
var LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
|
6
|
+
var NUMBERS = "0123456789";
|
|
7
|
+
var SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
|
8
|
+
function buildPool(options) {
|
|
9
|
+
let pool = "";
|
|
10
|
+
if (options.uppercase) pool += UPPERCASE;
|
|
11
|
+
if (options.lowercase) pool += LOWERCASE;
|
|
12
|
+
if (options.numbers) pool += NUMBERS;
|
|
13
|
+
if (options.symbols) pool += SYMBOLS;
|
|
14
|
+
return pool;
|
|
15
|
+
}
|
|
16
|
+
function generateSingle(length, pool) {
|
|
17
|
+
const poolLength = pool.length;
|
|
18
|
+
const limit = 256 - 256 % poolLength;
|
|
19
|
+
const chars = [];
|
|
20
|
+
while (chars.length < length) {
|
|
21
|
+
const remaining = length - chars.length;
|
|
22
|
+
const bufferSize = remaining * 2;
|
|
23
|
+
const randomBytes = new Uint8Array(bufferSize);
|
|
24
|
+
getRandomValues(randomBytes);
|
|
25
|
+
for (let i = 0; i < randomBytes.length && chars.length < length; i++) {
|
|
26
|
+
const byte = randomBytes[i];
|
|
27
|
+
if (byte < limit) {
|
|
28
|
+
chars.push(pool[byte % poolLength]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return chars.join("");
|
|
33
|
+
}
|
|
34
|
+
function validateOptions(options) {
|
|
35
|
+
if (!Number.isInteger(options.length) || options.length < 1) {
|
|
36
|
+
throw new Error("\uAE38\uC774\uB294 1~1024 \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
37
|
+
}
|
|
38
|
+
if (options.length > 1024) {
|
|
39
|
+
throw new Error("\uAE38\uC774\uB294 1~1024 \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
40
|
+
}
|
|
41
|
+
if (!Number.isInteger(options.count) || options.count < 1) {
|
|
42
|
+
throw new Error("count\uB294 1 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
43
|
+
}
|
|
44
|
+
if (!options.uppercase && !options.lowercase && !options.numbers && !options.symbols) {
|
|
45
|
+
throw new Error("\uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uBB38\uC790\uC14B\uC744 \uD3EC\uD568\uD574\uC57C \uD569\uB2C8\uB2E4");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function generatePassword(options) {
|
|
49
|
+
const resolved = {
|
|
50
|
+
length: options?.length ?? 16,
|
|
51
|
+
count: options?.count ?? 1,
|
|
52
|
+
uppercase: options?.uppercase ?? true,
|
|
53
|
+
lowercase: options?.lowercase ?? true,
|
|
54
|
+
numbers: options?.numbers ?? true,
|
|
55
|
+
symbols: options?.symbols ?? true
|
|
56
|
+
};
|
|
57
|
+
validateOptions(resolved);
|
|
58
|
+
const pool = buildPool(resolved);
|
|
59
|
+
const passwords = [];
|
|
60
|
+
for (let i = 0; i < resolved.count; i++) {
|
|
61
|
+
passwords.push(generateSingle(resolved.length, pool));
|
|
62
|
+
}
|
|
63
|
+
return passwords;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { generatePassword };
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lockly",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cryptographically secure random password generator CLI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "Daniel Kyojun Ku <d@nielku.com>",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"password",
|
|
10
|
+
"generator",
|
|
11
|
+
"cli",
|
|
12
|
+
"crypto",
|
|
13
|
+
"random",
|
|
14
|
+
"secure"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/kjunine/lockly.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/kjunine/lockly/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/kjunine/lockly#readme",
|
|
24
|
+
"bin": {
|
|
25
|
+
"lockly": "./dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"main": "./dist/index.cjs",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"require": "./dist/index.cjs",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"lint": "eslint src tests",
|
|
46
|
+
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
47
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:coverage": "vitest run --coverage",
|
|
50
|
+
"check": "pnpm run lint && pnpm run format:check && pnpm run build && pnpm run test"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"commander": "^14.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@eslint/js": "^9.0.0",
|
|
57
|
+
"@types/node": "^22.0.0",
|
|
58
|
+
"@vitest/coverage-v8": "^4.0.0",
|
|
59
|
+
"eslint": "^9.0.0",
|
|
60
|
+
"eslint-config-prettier": "^10.1.8",
|
|
61
|
+
"prettier": "^3.2.0",
|
|
62
|
+
"tsup": "^8.0.0",
|
|
63
|
+
"typescript": "^5.3.0",
|
|
64
|
+
"typescript-eslint": "^8.0.0",
|
|
65
|
+
"vitest": "^4.0.0"
|
|
66
|
+
},
|
|
67
|
+
"packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
|
|
68
|
+
}
|